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