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;
038
039import org.apache.logging.log4j.LoggingException;
040import org.apache.logging.log4j.core.Layout;
041import org.apache.logging.log4j.core.LogEvent;
042import org.apache.logging.log4j.core.appender.AbstractManager;
043import org.apache.logging.log4j.core.appender.ManagerFactory;
044import org.apache.logging.log4j.core.util.CyclicBuffer;
045import org.apache.logging.log4j.core.util.NameUtil;
046import org.apache.logging.log4j.core.util.NetUtils;
047import org.apache.logging.log4j.util.PropertiesUtil;
048import org.apache.logging.log4j.util.Strings;
049
050/**
051 * Manager for sending SMTP events.
052 */
053public 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.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        try (final OutputStream encoder = MimeUtility.encode(out, encoding)) {
218            encoder.write(bytes);
219        }
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}