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.config.Configuration;
46  import org.apache.logging.log4j.core.impl.Log4jLogEvent;
47  import org.apache.logging.log4j.core.impl.MutableLogEvent;
48  import org.apache.logging.log4j.core.layout.AbstractStringLayout.Serializer;
49  import org.apache.logging.log4j.core.layout.PatternLayout;
50  import org.apache.logging.log4j.core.net.ssl.SslConfiguration;
51  import org.apache.logging.log4j.core.util.CyclicBuffer;
52  import org.apache.logging.log4j.core.util.NameUtil;
53  import org.apache.logging.log4j.core.util.NetUtils;
54  import org.apache.logging.log4j.message.ReusableMessage;
55  import org.apache.logging.log4j.util.PropertiesUtil;
56  import org.apache.logging.log4j.util.Strings;
57  
58  /**
59   * Manager for sending SMTP events.
60   */
61  public class SmtpManager extends AbstractManager {
62      private static final SMTPManagerFactory FACTORY = new SMTPManagerFactory();
63  
64      private final Session session;
65  
66      private final CyclicBuffer<LogEvent> buffer;
67  
68      private volatile MimeMessage message;
69  
70      private final FactoryData data;
71  
72      private static MimeMessage createMimeMessage(final FactoryData data, final Session session, final LogEvent appendEvent)
73              throws MessagingException {
74          return new MimeMessageBuilder(session).setFrom(data.from).setReplyTo(data.replyto)
75                  .setRecipients(Message.RecipientType.TO, data.to).setRecipients(Message.RecipientType.CC, data.cc)
76                  .setRecipients(Message.RecipientType.BCC, data.bcc).setSubject(data.subject.toSerializable(appendEvent))
77                  .build();
78      }
79  
80      protected SmtpManager(final String name, final Session session, final MimeMessage message,
81                            final FactoryData data) {
82          super(null, name);
83          this.session = session;
84          this.message = message;
85          this.data = data;
86          this.buffer = new CyclicBuffer<>(LogEvent.class, data.numElements);
87      }
88  
89      public void add(LogEvent event) {
90          if (event instanceof Log4jLogEvent && event.getMessage() instanceof ReusableMessage) {
91              ((Log4jLogEvent) event).makeMessageImmutable();
92          } else if (event instanceof MutableLogEvent) {
93              event = ((MutableLogEvent) event).createMemento();
94          }
95          buffer.add(event);
96      }
97  
98      public static SmtpManager getSmtpManager(
99                                               final Configuration config,
100                                              final String to, final String cc, final String bcc,
101                                              final String from, final String replyTo,
102                                              final String subject, String protocol, final String host,
103                                              final int port, final String username, final String password,
104                                              final boolean isDebug, final String filterName, final int numElements,
105                                              final SslConfiguration sslConfiguration) {
106         if (Strings.isEmpty(protocol)) {
107             protocol = "smtp";
108         }
109 
110         final StringBuilder sb = new StringBuilder();
111         if (to != null) {
112             sb.append(to);
113         }
114         sb.append(':');
115         if (cc != null) {
116             sb.append(cc);
117         }
118         sb.append(':');
119         if (bcc != null) {
120             sb.append(bcc);
121         }
122         sb.append(':');
123         if (from != null) {
124             sb.append(from);
125         }
126         sb.append(':');
127         if (replyTo != null) {
128             sb.append(replyTo);
129         }
130         sb.append(':');
131         if (subject != null) {
132             sb.append(subject);
133         }
134         sb.append(':');
135         sb.append(protocol).append(':').append(host).append(':').append("port").append(':');
136         if (username != null) {
137             sb.append(username);
138         }
139         sb.append(':');
140         if (password != null) {
141             sb.append(password);
142         }
143         sb.append(isDebug ? ":debug:" : "::");
144         sb.append(filterName);
145 
146         final String name = "SMTP:" + NameUtil.md5(sb.toString());
147         final Serializer subjectSerializer = PatternLayout.newSerializerBuilder().setConfiguration(config).setPattern(subject).build();
148 
149         return getManager(name, FACTORY, new FactoryData(to, cc, bcc, from, replyTo, subjectSerializer,
150             protocol, host, port, username, password, isDebug, numElements, sslConfiguration));
151     }
152 
153     /**
154      * Send the contents of the cyclic buffer as an e-mail message.
155      * @param layout The layout for formatting the events.
156      * @param appendEvent The event that triggered the send.
157      */
158     public void sendEvents(final Layout<?> layout, final LogEvent appendEvent) {
159         if (message == null) {
160             connect(appendEvent);
161         }
162         try {
163             final LogEvent[] priorEvents = buffer.removeAll();
164             // LOG4J-310: log appendEvent even if priorEvents is empty
165 
166             final byte[] rawBytes = formatContentToBytes(priorEvents, appendEvent, layout);
167 
168             final String contentType = layout.getContentType();
169             final String encoding = getEncoding(rawBytes, contentType);
170             final byte[] encodedBytes = encodeContentToBytes(rawBytes, encoding);
171 
172             final InternetHeaders headers = getHeaders(contentType, encoding);
173             final MimeMultipart mp = getMimeMultipart(encodedBytes, headers);
174 
175             sendMultipartMessage(message, mp);
176         } catch (final MessagingException | IOException | RuntimeException e) {
177             logError("Caught exception while sending e-mail notification.", e);
178             throw new LoggingException("Error occurred while sending email", e);
179         }
180     }
181 
182     protected byte[] formatContentToBytes(final LogEvent[] priorEvents, final LogEvent appendEvent,
183                                           final Layout<?> layout) throws IOException {
184         final ByteArrayOutputStream raw = new ByteArrayOutputStream();
185         writeContent(priorEvents, appendEvent, layout, raw);
186         return raw.toByteArray();
187     }
188 
189     private void writeContent(final LogEvent[] priorEvents, final LogEvent appendEvent, final Layout<?> layout,
190                               final ByteArrayOutputStream out)
191         throws IOException {
192         writeHeader(layout, out);
193         writeBuffer(priorEvents, appendEvent, layout, out);
194         writeFooter(layout, out);
195     }
196 
197     protected void writeHeader(final Layout<?> layout, final OutputStream out) throws IOException {
198         final byte[] header = layout.getHeader();
199         if (header != null) {
200             out.write(header);
201         }
202     }
203 
204     protected void writeBuffer(final LogEvent[] priorEvents, final LogEvent appendEvent, final Layout<?> layout,
205                                final OutputStream out) throws IOException {
206         for (final LogEvent priorEvent : priorEvents) {
207             final byte[] bytes = layout.toByteArray(priorEvent);
208             out.write(bytes);
209         }
210 
211         final byte[] bytes = layout.toByteArray(appendEvent);
212         out.write(bytes);
213     }
214 
215     protected void writeFooter(final Layout<?> layout, final OutputStream out) throws IOException {
216         final byte[] footer = layout.getFooter();
217         if (footer != null) {
218             out.write(footer);
219         }
220     }
221 
222     protected String getEncoding(final byte[] rawBytes, final String contentType) {
223         final DataSource dataSource = new ByteArrayDataSource(rawBytes, contentType);
224         return MimeUtility.getEncoding(dataSource);
225     }
226 
227     protected byte[] encodeContentToBytes(final byte[] rawBytes, final String encoding)
228         throws MessagingException, IOException {
229         final ByteArrayOutputStream encoded = new ByteArrayOutputStream();
230         encodeContent(rawBytes, encoding, encoded);
231         return encoded.toByteArray();
232     }
233 
234     protected void encodeContent(final byte[] bytes, final String encoding, final ByteArrayOutputStream out)
235             throws MessagingException, IOException {
236         try (final OutputStream encoder = MimeUtility.encode(out, encoding)) {
237             encoder.write(bytes);
238         }
239     }
240 
241     protected InternetHeaders getHeaders(final String contentType, final String encoding) {
242         final InternetHeaders headers = new InternetHeaders();
243         headers.setHeader("Content-Type", contentType + "; charset=UTF-8");
244         headers.setHeader("Content-Transfer-Encoding", encoding);
245         return headers;
246     }
247 
248     protected MimeMultipart getMimeMultipart(final byte[] encodedBytes, final InternetHeaders headers)
249         throws MessagingException {
250         final MimeMultipart mp = new MimeMultipart();
251         final MimeBodyPart part = new MimeBodyPart(headers, encodedBytes);
252         mp.addBodyPart(part);
253         return mp;
254     }
255 
256     protected void sendMultipartMessage(final MimeMessage msg, final MimeMultipart mp) throws MessagingException {
257         synchronized (msg) {
258             msg.setContent(mp);
259             msg.setSentDate(new Date());
260             Transport.send(msg);
261         }
262     }
263 
264     /**
265      * Factory data.
266      */
267     private static class FactoryData {
268         private final String to;
269         private final String cc;
270         private final String bcc;
271         private final String from;
272         private final String replyto;
273         private final Serializer subject;
274         private final String protocol;
275         private final String host;
276         private final int port;
277         private final String username;
278         private final String password;
279         private final boolean isDebug;
280         private final int numElements;
281         private final SslConfiguration sslConfiguration;
282 
283         public FactoryData(final String to, final String cc, final String bcc, final String from, final String replyTo,
284                            final Serializer subjectSerializer, final String protocol, final String host, final int port,
285                            final String username, final String password, final boolean isDebug, final int numElements,
286                            final SslConfiguration sslConfiguration) {
287             this.to = to;
288             this.cc = cc;
289             this.bcc = bcc;
290             this.from = from;
291             this.replyto = replyTo;
292             this.subject = subjectSerializer;
293             this.protocol = protocol;
294             this.host = host;
295             this.port = port;
296             this.username = username;
297             this.password = password;
298             this.isDebug = isDebug;
299             this.numElements = numElements;
300             this.sslConfiguration = sslConfiguration;
301         }
302     }
303 
304     private synchronized void connect(final LogEvent appendEvent) {
305         if (message != null) {
306             return;
307         }
308         try {
309             message = createMimeMessage(data, session, appendEvent);
310         } catch (final MessagingException e) {
311             logError("Could not set SmtpAppender message options", e);
312             message = null;
313         }
314     }
315 
316     /**
317      * Factory to create the SMTP Manager.
318      */
319     private static class SMTPManagerFactory implements ManagerFactory<SmtpManager, FactoryData> {
320 
321         @Override
322         public SmtpManager createManager(final String name, final FactoryData data) {
323             final String prefix = "mail." + data.protocol;
324 
325             final Properties properties = PropertiesUtil.getSystemProperties();
326             properties.setProperty("mail.transport.protocol", data.protocol);
327             if (properties.getProperty("mail.host") == null) {
328                 // Prevent an UnknownHostException in Java 7
329                 properties.setProperty("mail.host", NetUtils.getLocalHostname());
330             }
331 
332             if (null != data.host) {
333                 properties.setProperty(prefix + ".host", data.host);
334             }
335             if (data.port > 0) {
336                 properties.setProperty(prefix + ".port", String.valueOf(data.port));
337             }
338 
339             final Authenticator authenticator = buildAuthenticator(data.username, data.password);
340             if (null != authenticator) {
341                 properties.setProperty(prefix + ".auth", "true");
342             }
343 
344             if (data.protocol.equals("smtps")) {
345                 final SslConfiguration sslConfiguration = data.sslConfiguration;
346                 if (sslConfiguration != null) {
347                     final SSLSocketFactory sslSocketFactory = sslConfiguration.getSslSocketFactory();
348                     properties.put(prefix + ".ssl.socketFactory", sslSocketFactory);
349                     properties.setProperty(prefix + ".ssl.checkserveridentity", Boolean.toString(sslConfiguration.isVerifyHostName()));
350                 }
351             }
352 
353             final Session session = Session.getInstance(properties, authenticator);
354             session.setProtocolForAddress("rfc822", data.protocol);
355             session.setDebug(data.isDebug);
356             return new SmtpManager(name, session, null, data);
357         }
358 
359         private Authenticator buildAuthenticator(final String username, final String password) {
360             if (null != password && null != username) {
361                 return new Authenticator() {
362                     private final PasswordAuthentication passwordAuthentication =
363                         new PasswordAuthentication(username, password);
364 
365                     @Override
366                     protected PasswordAuthentication getPasswordAuthentication() {
367                         return passwordAuthentication;
368                     }
369                 };
370             }
371             return null;
372         }
373     }
374 }