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     */
017    package org.apache.logging.log4j.core.net;
018    
019    import java.io.ByteArrayOutputStream;
020    import java.io.IOException;
021    import java.io.OutputStream;
022    import java.util.Date;
023    import java.util.Properties;
024    
025    import javax.activation.DataSource;
026    import javax.mail.Authenticator;
027    import javax.mail.Message;
028    import javax.mail.MessagingException;
029    import javax.mail.PasswordAuthentication;
030    import javax.mail.Session;
031    import javax.mail.Transport;
032    import javax.mail.internet.InternetHeaders;
033    import javax.mail.internet.MimeBodyPart;
034    import javax.mail.internet.MimeMessage;
035    import javax.mail.internet.MimeMultipart;
036    import javax.mail.internet.MimeUtility;
037    import javax.mail.util.ByteArrayDataSource;
038    
039    import org.apache.logging.log4j.LoggingException;
040    import org.apache.logging.log4j.core.Layout;
041    import org.apache.logging.log4j.core.LogEvent;
042    import org.apache.logging.log4j.core.appender.AbstractManager;
043    import org.apache.logging.log4j.core.appender.ManagerFactory;
044    import org.apache.logging.log4j.core.util.CyclicBuffer;
045    import org.apache.logging.log4j.core.util.NameUtil;
046    import org.apache.logging.log4j.core.util.NetUtils;
047    import org.apache.logging.log4j.util.PropertiesUtil;
048    import org.apache.logging.log4j.util.Strings;
049    
050    /**
051     * Manager for sending SMTP events.
052     */
053    public 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>(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            final OutputStream encoder = MimeUtility.encode(out, encoding);
218            encoder.write(bytes);
219            encoder.close();
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    }