View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements. See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache license, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License. You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the license for the specific language governing permissions and
15   * limitations under the license.
16   */
17  package org.apache.logging.log4j.core.net;
18  
19  import java.io.ByteArrayOutputStream;
20  import java.io.IOException;
21  import java.io.OutputStream;
22  import java.util.Date;
23  import java.util.Properties;
24  
25  import javax.activation.DataSource;
26  import javax.mail.Authenticator;
27  import javax.mail.Message;
28  import javax.mail.MessagingException;
29  import javax.mail.PasswordAuthentication;
30  import javax.mail.Session;
31  import javax.mail.Transport;
32  import javax.mail.internet.InternetHeaders;
33  import javax.mail.internet.MimeBodyPart;
34  import javax.mail.internet.MimeMessage;
35  import javax.mail.internet.MimeMultipart;
36  import javax.mail.internet.MimeUtility;
37  import javax.mail.util.ByteArrayDataSource;
38  import javax.net.ssl.SSLSocketFactory;
39  
40  import org.apache.logging.log4j.LoggingException;
41  import org.apache.logging.log4j.core.Layout;
42  import org.apache.logging.log4j.core.LogEvent;
43  import org.apache.logging.log4j.core.appender.AbstractManager;
44  import org.apache.logging.log4j.core.appender.ManagerFactory;
45  import org.apache.logging.log4j.core.net.ssl.SslConfiguration;
46  import org.apache.logging.log4j.core.util.CyclicBuffer;
47  import org.apache.logging.log4j.core.util.NameUtil;
48  import org.apache.logging.log4j.core.util.NetUtils;
49  import org.apache.logging.log4j.util.PropertiesUtil;
50  import org.apache.logging.log4j.util.Strings;
51  
52  /**
53   * Manager for sending SMTP events.
54   */
55  public class SmtpManager extends AbstractManager {
56      private static final SMTPManagerFactory FACTORY = new SMTPManagerFactory();
57  
58      private final Session session;
59  
60      private final CyclicBuffer<LogEvent> buffer;
61  
62      private volatile MimeMessage message;
63  
64      private final FactoryData data;
65  
66      protected SmtpManager(final String name, final Session session, final MimeMessage message,
67                            final FactoryData data) {
68          super(name);
69          this.session = session;
70          this.message = message;
71          this.data = data;
72          this.buffer = new CyclicBuffer<LogEvent>(LogEvent.class, data.numElements);
73      }
74  
75      public void add(final LogEvent event) {
76          buffer.add(event);
77      }
78  
79      /**
80       * @deprecated use {@link #getSMTPManager(String, String, String, String, String, String, String, String, int, String, String, boolean, String, int, SslConfiguration)}
81       */
82      @Deprecated
83      public static SmtpManager getSMTPManager(
84              final String to, final String cc, final String bcc,
85              final String from, final String replyTo,
86              final String subject, String protocol, final String host,
87              final int port, final String username, final String password,
88              final boolean isDebug, final String filterName, final int numElements) {
89          return getSMTPManager(to, cc, bcc, from, replyTo, subject, protocol, host, port, username, password, isDebug,
90                  filterName, numElements, null);
91      }
92  
93      public static SmtpManager getSMTPManager(
94              final String to, final String cc, final String bcc,
95              final String from, final String replyTo,
96              final String subject, String protocol, final String host,
97              final int port, final String username, final String password,
98              final boolean isDebug, final String filterName, final int numElements,
99              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 }