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  
39  import org.apache.logging.log4j.LoggingException;
40  import org.apache.logging.log4j.core.Layout;
41  import org.apache.logging.log4j.core.LogEvent;
42  import org.apache.logging.log4j.core.appender.AbstractManager;
43  import org.apache.logging.log4j.core.appender.ManagerFactory;
44  import org.apache.logging.log4j.core.config.Configuration;
45  import org.apache.logging.log4j.core.impl.Log4jLogEvent;
46  import org.apache.logging.log4j.core.impl.MutableLogEvent;
47  import org.apache.logging.log4j.core.layout.AbstractStringLayout.Serializer;
48  import org.apache.logging.log4j.core.layout.PatternLayout;
49  import org.apache.logging.log4j.core.util.CyclicBuffer;
50  import org.apache.logging.log4j.core.util.NameUtil;
51  import org.apache.logging.log4j.core.util.NetUtils;
52  import org.apache.logging.log4j.message.ReusableMessage;
53  import org.apache.logging.log4j.util.PropertiesUtil;
54  import org.apache.logging.log4j.util.Strings;
55  
56  /**
57   * Manager for sending SMTP events.
58   */
59  public class SmtpManager extends AbstractManager {
60      private static final SMTPManagerFactory FACTORY = new SMTPManagerFactory();
61  
62      private final Session session;
63  
64      private final CyclicBuffer<LogEvent> buffer;
65  
66      private volatile MimeMessage message;
67  
68      private final FactoryData data;
69  
70      private static MimeMessage createMimeMessage(final FactoryData data, final Session session, final LogEvent appendEvent)
71              throws MessagingException {
72          return new MimeMessageBuilder(session).setFrom(data.from).setReplyTo(data.replyto)
73                  .setRecipients(Message.RecipientType.TO, data.to).setRecipients(Message.RecipientType.CC, data.cc)
74                  .setRecipients(Message.RecipientType.BCC, data.bcc).setSubject(data.subject.toSerializable(appendEvent))
75                  .build();
76      }
77  
78      protected SmtpManager(final String name, final Session session, final MimeMessage message,
79                            final FactoryData data) {
80          super(null, name);
81          this.session = session;
82          this.message = message;
83          this.data = data;
84          this.buffer = new CyclicBuffer<>(LogEvent.class, data.numElements);
85      }
86  
87      public void add(LogEvent event) {
88          if (event instanceof Log4jLogEvent && event.getMessage() instanceof ReusableMessage) {
89              ((Log4jLogEvent) event).makeMessageImmutable();
90          } else if (event instanceof MutableLogEvent) {
91              event = ((MutableLogEvent) event).createMemento();
92          }
93          buffer.add(event);
94      }
95  
96      public static SmtpManager getSmtpManager(
97                                               final Configuration config,
98                                               final String to, final String cc, final String bcc,
99                                               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 }