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