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