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