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 */
017package org.apache.logging.log4j.core.net;
018
019import java.io.ByteArrayOutputStream;
020import java.io.IOException;
021import java.io.OutputStream;
022import java.util.Date;
023import java.util.Properties;
024
025import javax.activation.DataSource;
026import javax.mail.Authenticator;
027import javax.mail.Message;
028import javax.mail.MessagingException;
029import javax.mail.PasswordAuthentication;
030import javax.mail.Session;
031import javax.mail.Transport;
032import javax.mail.internet.InternetHeaders;
033import javax.mail.internet.MimeBodyPart;
034import javax.mail.internet.MimeMessage;
035import javax.mail.internet.MimeMultipart;
036import javax.mail.internet.MimeUtility;
037import javax.mail.util.ByteArrayDataSource;
038import javax.net.ssl.SSLSocketFactory;
039
040import org.apache.logging.log4j.LoggingException;
041import org.apache.logging.log4j.core.Layout;
042import org.apache.logging.log4j.core.LogEvent;
043import org.apache.logging.log4j.core.appender.AbstractManager;
044import org.apache.logging.log4j.core.appender.ManagerFactory;
045import org.apache.logging.log4j.core.config.Configuration;
046import org.apache.logging.log4j.core.impl.Log4jLogEvent;
047import org.apache.logging.log4j.core.impl.MutableLogEvent;
048import org.apache.logging.log4j.core.layout.AbstractStringLayout.Serializer;
049import org.apache.logging.log4j.core.layout.PatternLayout;
050import org.apache.logging.log4j.core.net.ssl.SslConfiguration;
051import org.apache.logging.log4j.core.util.CyclicBuffer;
052import org.apache.logging.log4j.core.util.NameUtil;
053import org.apache.logging.log4j.core.util.NetUtils;
054import org.apache.logging.log4j.message.ReusableMessage;
055import org.apache.logging.log4j.util.PropertiesUtil;
056import org.apache.logging.log4j.util.Strings;
057
058/**
059 * Manager for sending SMTP events.
060 */
061public class SmtpManager extends AbstractManager {
062    private static final SMTPManagerFactory FACTORY = new SMTPManagerFactory();
063
064    private final Session session;
065
066    private final CyclicBuffer<LogEvent> buffer;
067
068    private volatile MimeMessage message;
069
070    private final FactoryData data;
071
072    private static MimeMessage createMimeMessage(final FactoryData data, final Session session, final LogEvent appendEvent)
073            throws MessagingException {
074        return new MimeMessageBuilder(session).setFrom(data.from).setReplyTo(data.replyto)
075                .setRecipients(Message.RecipientType.TO, data.to).setRecipients(Message.RecipientType.CC, data.cc)
076                .setRecipients(Message.RecipientType.BCC, data.bcc).setSubject(data.subject.toSerializable(appendEvent))
077                .build();
078    }
079
080    protected SmtpManager(final String name, final Session session, final MimeMessage message,
081                          final FactoryData data) {
082        super(null, name);
083        this.session = session;
084        this.message = message;
085        this.data = data;
086        this.buffer = new CyclicBuffer<>(LogEvent.class, data.numElements);
087    }
088
089    public void add(LogEvent event) {
090        if (event instanceof Log4jLogEvent && event.getMessage() instanceof ReusableMessage) {
091            ((Log4jLogEvent) event).makeMessageImmutable();
092        } else if (event instanceof MutableLogEvent) {
093            event = ((MutableLogEvent) event).createMemento();
094        }
095        buffer.add(event);
096    }
097
098    public static SmtpManager getSmtpManager(
099                                             final Configuration config,
100                                             final String to, final String cc, final String bcc,
101                                             final String from, final String replyTo,
102                                             final String subject, String protocol, final String host,
103                                             final int port, final String username, final String password,
104                                             final boolean isDebug, final String filterName, final int numElements,
105                                             final SslConfiguration sslConfiguration) {
106        if (Strings.isEmpty(protocol)) {
107            protocol = "smtp";
108        }
109
110        final StringBuilder sb = new StringBuilder();
111        if (to != null) {
112            sb.append(to);
113        }
114        sb.append(':');
115        if (cc != null) {
116            sb.append(cc);
117        }
118        sb.append(':');
119        if (bcc != null) {
120            sb.append(bcc);
121        }
122        sb.append(':');
123        if (from != null) {
124            sb.append(from);
125        }
126        sb.append(':');
127        if (replyTo != null) {
128            sb.append(replyTo);
129        }
130        sb.append(':');
131        if (subject != null) {
132            sb.append(subject);
133        }
134        sb.append(':');
135        sb.append(protocol).append(':').append(host).append(':').append("port").append(':');
136        if (username != null) {
137            sb.append(username);
138        }
139        sb.append(':');
140        if (password != null) {
141            sb.append(password);
142        }
143        sb.append(isDebug ? ":debug:" : "::");
144        sb.append(filterName);
145
146        final String name = "SMTP:" + NameUtil.md5(sb.toString());
147        final Serializer subjectSerializer = PatternLayout.newSerializerBuilder().setConfiguration(config).setPattern(subject).build();
148
149        return getManager(name, FACTORY, new FactoryData(to, cc, bcc, from, replyTo, subjectSerializer,
150            protocol, host, port, username, password, isDebug, numElements, sslConfiguration));
151    }
152
153    /**
154     * Send the contents of the cyclic buffer as an e-mail message.
155     * @param layout The layout for formatting the events.
156     * @param appendEvent The event that triggered the send.
157     */
158    public void sendEvents(final Layout<?> layout, final LogEvent appendEvent) {
159        if (message == null) {
160            connect(appendEvent);
161        }
162        try {
163            final LogEvent[] priorEvents = buffer.removeAll();
164            // LOG4J-310: log appendEvent even if priorEvents is empty
165
166            final byte[] rawBytes = formatContentToBytes(priorEvents, appendEvent, layout);
167
168            final String contentType = layout.getContentType();
169            final String encoding = getEncoding(rawBytes, contentType);
170            final byte[] encodedBytes = encodeContentToBytes(rawBytes, encoding);
171
172            final InternetHeaders headers = getHeaders(contentType, encoding);
173            final MimeMultipart mp = getMimeMultipart(encodedBytes, headers);
174
175            sendMultipartMessage(message, mp);
176        } catch (final MessagingException | IOException | RuntimeException e) {
177            logError("Caught exception while sending e-mail notification.", e);
178            throw new LoggingException("Error occurred while sending email", e);
179        }
180    }
181
182    protected byte[] formatContentToBytes(final LogEvent[] priorEvents, final LogEvent appendEvent,
183                                          final Layout<?> layout) throws IOException {
184        final ByteArrayOutputStream raw = new ByteArrayOutputStream();
185        writeContent(priorEvents, appendEvent, layout, raw);
186        return raw.toByteArray();
187    }
188
189    private void writeContent(final LogEvent[] priorEvents, final LogEvent appendEvent, final Layout<?> layout,
190                              final ByteArrayOutputStream out)
191        throws IOException {
192        writeHeader(layout, out);
193        writeBuffer(priorEvents, appendEvent, layout, out);
194        writeFooter(layout, out);
195    }
196
197    protected void writeHeader(final Layout<?> layout, final OutputStream out) throws IOException {
198        final byte[] header = layout.getHeader();
199        if (header != null) {
200            out.write(header);
201        }
202    }
203
204    protected void writeBuffer(final LogEvent[] priorEvents, final LogEvent appendEvent, final Layout<?> layout,
205                               final OutputStream out) throws IOException {
206        for (final LogEvent priorEvent : priorEvents) {
207            final byte[] bytes = layout.toByteArray(priorEvent);
208            out.write(bytes);
209        }
210
211        final byte[] bytes = layout.toByteArray(appendEvent);
212        out.write(bytes);
213    }
214
215    protected void writeFooter(final Layout<?> layout, final OutputStream out) throws IOException {
216        final byte[] footer = layout.getFooter();
217        if (footer != null) {
218            out.write(footer);
219        }
220    }
221
222    protected String getEncoding(final byte[] rawBytes, final String contentType) {
223        final DataSource dataSource = new ByteArrayDataSource(rawBytes, contentType);
224        return MimeUtility.getEncoding(dataSource);
225    }
226
227    protected byte[] encodeContentToBytes(final byte[] rawBytes, final String encoding)
228        throws MessagingException, IOException {
229        final ByteArrayOutputStream encoded = new ByteArrayOutputStream();
230        encodeContent(rawBytes, encoding, encoded);
231        return encoded.toByteArray();
232    }
233
234    protected void encodeContent(final byte[] bytes, final String encoding, final ByteArrayOutputStream out)
235            throws MessagingException, IOException {
236        try (final OutputStream encoder = MimeUtility.encode(out, encoding)) {
237            encoder.write(bytes);
238        }
239    }
240
241    protected InternetHeaders getHeaders(final String contentType, final String encoding) {
242        final InternetHeaders headers = new InternetHeaders();
243        headers.setHeader("Content-Type", contentType + "; charset=UTF-8");
244        headers.setHeader("Content-Transfer-Encoding", encoding);
245        return headers;
246    }
247
248    protected MimeMultipart getMimeMultipart(final byte[] encodedBytes, final InternetHeaders headers)
249        throws MessagingException {
250        final MimeMultipart mp = new MimeMultipart();
251        final MimeBodyPart part = new MimeBodyPart(headers, encodedBytes);
252        mp.addBodyPart(part);
253        return mp;
254    }
255
256    protected void sendMultipartMessage(final MimeMessage msg, final MimeMultipart mp) throws MessagingException {
257        synchronized (msg) {
258            msg.setContent(mp);
259            msg.setSentDate(new Date());
260            Transport.send(msg);
261        }
262    }
263
264    /**
265     * Factory data.
266     */
267    private static class FactoryData {
268        private final String to;
269        private final String cc;
270        private final String bcc;
271        private final String from;
272        private final String replyto;
273        private final Serializer subject;
274        private final String protocol;
275        private final String host;
276        private final int port;
277        private final String username;
278        private final String password;
279        private final boolean isDebug;
280        private final int numElements;
281        private final SslConfiguration sslConfiguration;
282
283        public FactoryData(final String to, final String cc, final String bcc, final String from, final String replyTo,
284                           final Serializer subjectSerializer, final String protocol, final String host, final int port,
285                           final String username, final String password, final boolean isDebug, final int numElements,
286                           final SslConfiguration sslConfiguration) {
287            this.to = to;
288            this.cc = cc;
289            this.bcc = bcc;
290            this.from = from;
291            this.replyto = replyTo;
292            this.subject = subjectSerializer;
293            this.protocol = protocol;
294            this.host = host;
295            this.port = port;
296            this.username = username;
297            this.password = password;
298            this.isDebug = isDebug;
299            this.numElements = numElements;
300            this.sslConfiguration = sslConfiguration;
301        }
302    }
303
304    private synchronized void connect(final LogEvent appendEvent) {
305        if (message != null) {
306            return;
307        }
308        try {
309            message = createMimeMessage(data, session, appendEvent);
310        } catch (final MessagingException e) {
311            logError("Could not set SmtpAppender message options", e);
312            message = null;
313        }
314    }
315
316    /**
317     * Factory to create the SMTP Manager.
318     */
319    private static class SMTPManagerFactory implements ManagerFactory<SmtpManager, FactoryData> {
320
321        @Override
322        public SmtpManager createManager(final String name, final FactoryData data) {
323            final String prefix = "mail." + data.protocol;
324
325            final Properties properties = PropertiesUtil.getSystemProperties();
326            properties.setProperty("mail.transport.protocol", data.protocol);
327            if (properties.getProperty("mail.host") == null) {
328                // Prevent an UnknownHostException in Java 7
329                properties.setProperty("mail.host", NetUtils.getLocalHostname());
330            }
331
332            if (null != data.host) {
333                properties.setProperty(prefix + ".host", data.host);
334            }
335            if (data.port > 0) {
336                properties.setProperty(prefix + ".port", String.valueOf(data.port));
337            }
338
339            final Authenticator authenticator = buildAuthenticator(data.username, data.password);
340            if (null != authenticator) {
341                properties.setProperty(prefix + ".auth", "true");
342            }
343
344            if (data.protocol.equals("smtps")) {
345                final SslConfiguration sslConfiguration = data.sslConfiguration;
346                if (sslConfiguration != null) {
347                    final SSLSocketFactory sslSocketFactory = sslConfiguration.getSslSocketFactory();
348                    properties.put(prefix + ".ssl.socketFactory", sslSocketFactory);
349                    properties.setProperty(prefix + ".ssl.checkserveridentity", Boolean.toString(sslConfiguration.isVerifyHostName()));
350                }
351            }
352
353            final Session session = Session.getInstance(properties, authenticator);
354            session.setProtocolForAddress("rfc822", data.protocol);
355            session.setDebug(data.isDebug);
356            return new SmtpManager(name, session, null, data);
357        }
358
359        private Authenticator buildAuthenticator(final String username, final String password) {
360            if (null != password && null != username) {
361                return new Authenticator() {
362                    private final PasswordAuthentication passwordAuthentication =
363                        new PasswordAuthentication(username, password);
364
365                    @Override
366                    protected PasswordAuthentication getPasswordAuthentication() {
367                        return passwordAuthentication;
368                    }
369                };
370            }
371            return null;
372        }
373    }
374}