    001    /*
    002     * Licensed to the Apache Software Foundation (ASF) under one or more
    003     * contributor license agreements. See the NOTICE file distributed with
    004     * this work for additional information regarding copyright ownership.
    005     * The ASF licenses this file to You under the Apache license, Version 2.0
    006     * (the "License"); you may not use this file except in compliance with
    007     * the License. You may obtain a copy of the License at
    008     *
    009     *      http://www.apache.org/licenses/LICENSE-2.0
    010     *
    011     * Unless required by applicable law or agreed to in writing, software
    012     * distributed under the License is distributed on an "AS IS" BASIS,
    013     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    014     * See the license for the specific language governing permissions and
    015     * limitations under the license.
    016     */
    017    package org.apache.logging.log4j.core.net;
    018
    019    import java.io.ByteArrayOutputStream;
    020    import java.io.IOException;
    021    import java.io.OutputStream;
    022    import java.util.Date;
    023    import java.util.Properties;
    024
    025    import javax.activation.DataSource;
    026    import javax.mail.Authenticator;
    027    import javax.mail.Message;
    028    import javax.mail.MessagingException;
    029    import javax.mail.PasswordAuthentication;
    030    import javax.mail.Session;
    031    import javax.mail.Transport;
    032    import javax.mail.internet.InternetHeaders;
    033    import javax.mail.internet.MimeBodyPart;
    034    import javax.mail.internet.MimeMessage;
    035    import javax.mail.internet.MimeMultipart;
    036    import javax.mail.internet.MimeUtility;
    037    import javax.mail.util.ByteArrayDataSource;
    038
    039    import org.apache.logging.log4j.LoggingException;
    040    import org.apache.logging.log4j.core.Layout;
    041    import org.apache.logging.log4j.core.LogEvent;
    042    import org.apache.logging.log4j.core.appender.AbstractManager;
    043    import org.apache.logging.log4j.core.appender.ManagerFactory;
    044    import org.apache.logging.log4j.core.util.CyclicBuffer;
    045    import org.apache.logging.log4j.core.util.NameUtil;
    046    import org.apache.logging.log4j.core.util.NetUtils;
    047    import org.apache.logging.log4j.util.PropertiesUtil;
    048    import org.apache.logging.log4j.util.Strings;
    049
    050    /**
    051     * Manager for sending SMTP events.
    052     */
    053    public class SmtpManager extends AbstractManager {
    054        private static final SMTPManagerFactory FACTORY = new SMTPManagerFactory();
    055
    056        private final Session session;
    057
    058        private final CyclicBuffer<LogEvent> buffer;
    059
    060        private volatile MimeMessage message;
    061
    062        private final FactoryData data;
    063
    064        protected SmtpManager(final String name, final Session session, final MimeMessage message,
    065                              final FactoryData data) {
    066            super(name);
    067            this.session = session;
    068            this.message = message;
    069            this.data = data;
    070            this.buffer = new CyclicBuffer<LogEvent>(LogEvent.class, data.numElements);
    071        }
    072
    073        public void add(final LogEvent event) {
    074            buffer.add(event);
    075        }
    076
    077        public static SmtpManager getSMTPManager(final String to, final String cc, final String bcc,
    078                                                 final String from, final String replyTo,
    079                                                 final String subject, String protocol, final String host,
    080                                                 final int port, final String username, final String password,
    081                                                 final boolean isDebug, final String filterName, final int numElements) {
    082            if (Strings.isEmpty(protocol)) {
    083                protocol = "smtp";
    084            }
    085
    086            final StringBuilder sb = new StringBuilder();
    087            if (to != null) {
    088                sb.append(to);
    089            }
    090            sb.append(':');
    091            if (cc != null) {
    092                sb.append(cc);
    093            }
    094            sb.append(':');
    095            if (bcc != null) {
    096                sb.append(bcc);
    097            }
    098            sb.append(':');
    099            if (from != null) {
    100                sb.append(from);
    101            }
    102            sb.append(':');
    103            if (replyTo != null) {
    104                sb.append(replyTo);
    105            }
    106            sb.append(':');
    107            if (subject != null) {
    108                sb.append(subject);
    109            }
    110            sb.append(':');
    111            sb.append(protocol).append(':').append(host).append(':').append("port").append(':');
    112            if (username != null) {
    113                sb.append(username);
    114            }
    115            sb.append(':');
    116            if (password != null) {
    117                sb.append(password);
    118            }
    119            sb.append(isDebug ? ":debug:" : "::");
    120            sb.append(filterName);
    121
    122            final String name = "SMTP:" + NameUtil.md5(sb.toString());
    123
    124            return getManager(name, FACTORY, new FactoryData(to, cc, bcc, from, replyTo, subject,
    125                protocol, host, port, username, password, isDebug, numElements));
    126        }
    127
    128        /**
    129         * Send the contents of the cyclic buffer as an e-mail message.
    130         * @param layout The layout for formatting the events.
    131         * @param appendEvent The event that triggered the send.
    132         */
    133        public void sendEvents(final Layout<?> layout, final LogEvent appendEvent) {
    134            if (message == null) {
    135                connect();
    136            }
    137            try {
    138                final LogEvent[] priorEvents = buffer.removeAll();
    139                // LOG4J-310: log appendEvent even if priorEvents is empty
    140
    141                final byte[] rawBytes = formatContentToBytes(priorEvents, appendEvent, layout);
    142
    143                final String contentType = layout.getContentType();
    144                final String encoding = getEncoding(rawBytes, contentType);
    145                final byte[] encodedBytes = encodeContentToBytes(rawBytes, encoding);
    146
    147                final InternetHeaders headers = getHeaders(contentType, encoding);
    148                final MimeMultipart mp = getMimeMultipart(encodedBytes, headers);
    149
    150                sendMultipartMessage(message, mp);
    151            } catch (final MessagingException e) {
    152                LOGGER.error("Error occurred while sending e-mail notification.", e);
    153                throw new LoggingException("Error occurred while sending email", e);
    154            } catch (final IOException e) {
    155                LOGGER.error("Error occurred while sending e-mail notification.", e);
    156                throw new LoggingException("Error occurred while sending email", e);
    157            } catch (final RuntimeException e) {
    158                LOGGER.error("Error occurred while sending e-mail notification.", e);
    159                throw new LoggingException("Error occurred while sending email", e);
    160            }
    161        }
    162
    163        protected byte[] formatContentToBytes(final LogEvent[] priorEvents, final LogEvent appendEvent,
    164                                              final Layout<?> layout) throws IOException {
    165            final ByteArrayOutputStream raw = new ByteArrayOutputStream();
    166            writeContent(priorEvents, appendEvent, layout, raw);
    167            return raw.toByteArray();
    168        }
    169
    170        private void writeContent(final LogEvent[] priorEvents, final LogEvent appendEvent, final Layout<?> layout,
    171                                  final ByteArrayOutputStream out)
    172            throws IOException {
    173            writeHeader(layout, out);
    174            writeBuffer(priorEvents, appendEvent, layout, out);
    175            writeFooter(layout, out);
    176        }
    177
    178        protected void writeHeader(final Layout<?> layout, final OutputStream out) throws IOException {
    179            final byte[] header = layout.getHeader();
    180            if (header != null) {
    181                out.write(header);
    182            }
    183        }
    184
    185        protected void writeBuffer(final LogEvent[] priorEvents, final LogEvent appendEvent, final Layout<?> layout,
    186                                   final OutputStream out) throws IOException {
    187            for (final LogEvent priorEvent : priorEvents) {
    188                final byte[] bytes = layout.toByteArray(priorEvent);
    189                out.write(bytes);
    190            }
    191
    192            final byte[] bytes = layout.toByteArray(appendEvent);
    193            out.write(bytes);
    194        }
    195
    196        protected void writeFooter(final Layout<?> layout, final OutputStream out) throws IOException {
    197            final byte[] footer = layout.getFooter();
    198            if (footer != null) {
    199                out.write(footer);
    200            }
    201        }
    202
    203        protected String getEncoding(final byte[] rawBytes, final String contentType) {
    204            final DataSource dataSource = new ByteArrayDataSource(rawBytes, contentType);
    205            return MimeUtility.getEncoding(dataSource);
    206        }
    207
    208        protected byte[] encodeContentToBytes(final byte[] rawBytes, final String encoding)
    209            throws MessagingException, IOException {
    210            final ByteArrayOutputStream encoded = new ByteArrayOutputStream();
    211            encodeContent(rawBytes, encoding, encoded);
    212            return encoded.toByteArray();
    213        }
    214
    215        protected void encodeContent(final byte[] bytes, final String encoding, final ByteArrayOutputStream out)
    216            throws MessagingException, IOException {
    217            final OutputStream encoder = MimeUtility.encode(out, encoding);
    218            encoder.write(bytes);
    219            encoder.close();
    220        }
    221
    222        protected InternetHeaders getHeaders(final String contentType, final String encoding) {
    223            final InternetHeaders headers = new InternetHeaders();
    224            headers.setHeader("Content-Type", contentType + "; charset=UTF-8");
    225            headers.setHeader("Content-Transfer-Encoding", encoding);
    226            return headers;
    227        }
    228
    229        protected MimeMultipart getMimeMultipart(final byte[] encodedBytes, final InternetHeaders headers)
    230            throws MessagingException {
    231            final MimeMultipart mp = new MimeMultipart();
    232            final MimeBodyPart part = new MimeBodyPart(headers, encodedBytes);
    233            mp.addBodyPart(part);
    234            return mp;
    235        }
    236
    237        protected void sendMultipartMessage(final MimeMessage message, final MimeMultipart mp) throws MessagingException {
    238            synchronized (message) {
    239                message.setContent(mp);
    240                message.setSentDate(new Date());
    241                Transport.send(message);
    242            }
    243        }
    244
    245        /**
    246         * Factory data.
    247         */
    248        private static class FactoryData {
    249            private final String to;
    250            private final String cc;
    251            private final String bcc;
    252            private final String from;
    253            private final String replyto;
    254            private final String subject;
    255            private final String protocol;
    256            private final String host;
    257            private final int port;
    258            private final String username;
    259            private final String password;
    260            private final boolean isDebug;
    261            private final int numElements;
    262
    263            public FactoryData(final String to, final String cc, final String bcc, final String from, final String replyTo,
    264                               final String subject, final String protocol, final String host, final int port,
    265                               final String username, final String password, final boolean isDebug, final int numElements) {
    266                this.to = to;
    267                this.cc = cc;
    268                this.bcc = bcc;
    269                this.from = from;
    270                this.replyto = replyTo;
    271                this.subject = subject;
    272                this.protocol = protocol;
    273                this.host = host;
    274                this.port = port;
    275                this.username = username;
    276                this.password = password;
    277                this.isDebug = isDebug;
    278                this.numElements = numElements;
    279            }
    280        }
    281
    282        private synchronized void connect() {
    283            if (message != null) {
    284                return;
    285            }
    286            try {
    287                message = new MimeMessageBuilder(session).setFrom(data.from).setReplyTo(data.replyto)
    288                    .setRecipients(Message.RecipientType.TO, data.to).setRecipients(Message.RecipientType.CC, data.cc)
    289                    .setRecipients(Message.RecipientType.BCC, data.bcc).setSubject(data.subject).getMimeMessage();
    290            } catch (final MessagingException e) {
    291                LOGGER.error("Could not set SmtpAppender message options.", e);
    292                message = null;
    293            }
    294        }
    295
    296        /**
    297         * Factory to create the SMTP Manager.
    298         */
    299        private static class SMTPManagerFactory implements ManagerFactory<SmtpManager, FactoryData> {
    300
    301            @Override
    302            public SmtpManager createManager(final String name, final FactoryData data) {
    303                final String prefix = "mail." + data.protocol;
    304
    305                final Properties properties = PropertiesUtil.getSystemProperties();
    306                properties.put("mail.transport.protocol", data.protocol);
    307                if (properties.getProperty("mail.host") == null) {
    308                    // Prevent an UnknownHostException in Java 7
    309                    properties.put("mail.host", NetUtils.getLocalHostname());
    310                }
    311
    312                if (null != data.host) {
    313                    properties.put(prefix + ".host", data.host);
    314                }
    315                if (data.port > 0) {
    316                    properties.put(prefix + ".port", String.valueOf(data.port));
    317                }
    318
    319                final Authenticator authenticator = buildAuthenticator(data.username, data.password);
    320                if (null != authenticator) {
    321                    properties.put(prefix + ".auth", "true");
    322                }
    323
    324                final Session session = Session.getInstance(properties, authenticator);
    325                session.setProtocolForAddress("rfc822", data.protocol);
    326                session.setDebug(data.isDebug);
    327                MimeMessage message;
    328
    329                try {
    330                    message = new MimeMessageBuilder(session).setFrom(data.from).setReplyTo(data.replyto)
    331                        .setRecipients(Message.RecipientType.TO, data.to).setRecipients(Message.RecipientType.CC, data.cc)
    332                        .setRecipients(Message.RecipientType.BCC, data.bcc).setSubject(data.subject).getMimeMessage();
    333                } catch (final MessagingException e) {
    334                    LOGGER.error("Could not set SmtpAppender message options.", e);
    335                    message = null;
    336                }
    337
    338                return new SmtpManager(name, session, message, data);
    339            }
    340
    341            private Authenticator buildAuthenticator(final String username, final String password) {
    342                if (null != password && null != username) {
    343                    return new Authenticator() {
    344                        private final PasswordAuthentication passwordAuthentication =
    345                            new PasswordAuthentication(username, password);
    346
    347                        @Override
    348                        protected PasswordAuthentication getPasswordAuthentication() {
    349                            return passwordAuthentication;
    350                        }
    351                    };
    352                }
    353                return null;
    354            }
    355        }
    356    }