1
2
3
4
5
6
7
8
9
10
11
12
13
14 package ch.qos.logback.core.net;
15
16 import java.util.ArrayList;
17 import java.util.Arrays;
18 import java.util.Date;
19 import java.util.List;
20 import java.util.Properties;
21 import java.util.concurrent.Future;
22
23 import jakarta.mail.Message;
24 import jakarta.mail.Multipart;
25 import jakarta.mail.Session;
26 import jakarta.mail.Transport;
27 import jakarta.mail.internet.AddressException;
28 import jakarta.mail.internet.InternetAddress;
29 import jakarta.mail.internet.MimeBodyPart;
30 import jakarta.mail.internet.MimeMessage;
31 import jakarta.mail.internet.MimeMultipart;
32 import javax.naming.Context;
33
34 import ch.qos.logback.core.AppenderBase;
35 import ch.qos.logback.core.CoreConstants;
36 import ch.qos.logback.core.Layout;
37 import ch.qos.logback.core.boolex.EvaluationException;
38 import ch.qos.logback.core.boolex.EventEvaluator;
39 import ch.qos.logback.core.helpers.CyclicBuffer;
40 import ch.qos.logback.core.pattern.PatternLayoutBase;
41 import ch.qos.logback.core.sift.DefaultDiscriminator;
42 import ch.qos.logback.core.sift.Discriminator;
43 import ch.qos.logback.core.spi.CyclicBufferTracker;
44 import ch.qos.logback.core.util.ContentTypeUtil;
45 import ch.qos.logback.core.util.JNDIUtil;
46 import ch.qos.logback.core.util.OptionHelper;
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61 public abstract class SMTPAppenderBase<E> extends AppenderBase<E> {
62
63 static InternetAddress[] EMPTY_IA_ARRAY = new InternetAddress[0];
64
65 static final long MAX_DELAY_BETWEEN_STATUS_MESSAGES = 1228800 * CoreConstants.MILLIS_IN_ONE_SECOND;
66
67 long lastTrackerStatusPrint = 0;
68 long delayBetweenStatusMessages = 300 * CoreConstants.MILLIS_IN_ONE_SECOND;
69
70 protected Layout<E> subjectLayout;
71 protected Layout<E> layout;
72
73 private List<PatternLayoutBase<E>> toPatternLayoutList = new ArrayList<PatternLayoutBase<E>>();
74 private String from;
75 private String subjectStr = null;
76 private String smtpHost;
77 private int smtpPort = 25;
78 private boolean starttls = false;
79 private boolean ssl = false;
80 private boolean sessionViaJNDI = false;
81 private String jndiLocation = CoreConstants.JNDI_COMP_PREFIX + "/mail/Session";
82
83 String username;
84 String password;
85 String localhost;
86
87 boolean asynchronousSending = true;
88 protected Future<?> asynchronousSendingFuture = null;
89 private String charsetEncoding = "UTF-8";
90
91 protected Session session;
92
93 protected EventEvaluator<E> eventEvaluator;
94
95 protected Discriminator<E> discriminator = new DefaultDiscriminator<E>();
96 protected CyclicBufferTracker<E> cbTracker;
97
98 private int errorCount = 0;
99
100
101
102
103
104
105
106
107
108 abstract protected Layout<E> makeSubjectLayout(String subjectStr);
109
110
111
112
113 public void start() {
114
115 if (cbTracker == null) {
116 cbTracker = new CyclicBufferTracker<E>();
117 }
118
119 if (sessionViaJNDI)
120 session = lookupSessionInJNDI();
121 else
122 session = buildSessionFromProperties();
123
124 if (session == null) {
125 addError("Failed to obtain javax.mail.Session. Cannot start.");
126 return;
127 }
128
129 subjectLayout = makeSubjectLayout(subjectStr);
130
131 started = true;
132 }
133
134 private Session lookupSessionInJNDI() {
135 addInfo("Looking up javax.mail.Session at JNDI location [" + jndiLocation + "]");
136 try {
137 Context initialContext = JNDIUtil.getInitialContext();
138 Object obj = JNDIUtil.lookupObject(initialContext, jndiLocation);
139 return (Session) obj;
140 } catch (Exception e) {
141 addError("Failed to obtain javax.mail.Session from JNDI location [" + jndiLocation + "]", e);
142 return null;
143 }
144 }
145
146 private Session buildSessionFromProperties() {
147 Properties props = new Properties(OptionHelper.getSystemProperties());
148 if (smtpHost != null) {
149 props.put("mail.smtp.host", smtpHost);
150 }
151 props.put("mail.smtp.port", Integer.toString(smtpPort));
152
153 if (localhost != null) {
154 props.put("mail.smtp.localhost", localhost);
155 }
156
157 LoginAuthenticator loginAuthenticator = null;
158
159 if (!OptionHelper.isNullOrEmptyOrAllSpaces(username)) {
160 loginAuthenticator = new LoginAuthenticator(username, password);
161 props.put("mail.smtp.auth", "true");
162 }
163
164 if (isSTARTTLS() && isSSL()) {
165 addError("Both SSL and StartTLS cannot be enabled simultaneously");
166 } else {
167 if (isSTARTTLS()) {
168
169 props.put("mail.smtp.starttls.enable", "true");
170 props.put("mail.transport.protocol", "true");
171 }
172 if (isSSL()) {
173 props.put("mail.smtp.ssl.enable", "true");
174 }
175 }
176
177
178
179 return Session.getInstance(props, loginAuthenticator);
180 }
181
182
183
184
185
186 protected void append(E eventObject) {
187
188 if (!checkEntryConditions()) {
189 return;
190 }
191
192 String key = discriminator.getDiscriminatingValue(eventObject);
193 long now = System.currentTimeMillis();
194 final CyclicBuffer<E> cb = cbTracker.getOrCreate(key, now);
195 subAppend(cb, eventObject);
196
197 try {
198 if (eventEvaluator.evaluate(eventObject)) {
199
200 CyclicBuffer<E> cbClone = new CyclicBuffer<E>(cb);
201
202 cb.clear();
203
204 if (asynchronousSending) {
205
206 SenderRunnable senderRunnable = new SenderRunnable(cbClone, eventObject);
207 this.asynchronousSendingFuture = context.getExecutorService().submit(senderRunnable);
208 } else {
209
210 sendBuffer(cbClone, eventObject);
211 }
212 }
213 } catch (EvaluationException ex) {
214 errorCount++;
215 if (errorCount < CoreConstants.MAX_ERROR_COUNT) {
216 addError("SMTPAppender's EventEvaluator threw an Exception-", ex);
217 }
218 }
219
220
221 if (eventMarksEndOfLife(eventObject)) {
222 cbTracker.endOfLife(key);
223 }
224
225 cbTracker.removeStaleComponents(now);
226
227 if (lastTrackerStatusPrint + delayBetweenStatusMessages < now) {
228 addInfo("SMTPAppender [" + name + "] is tracking [" + cbTracker.getComponentCount() + "] buffers");
229 lastTrackerStatusPrint = now;
230
231 if (delayBetweenStatusMessages < MAX_DELAY_BETWEEN_STATUS_MESSAGES) {
232 delayBetweenStatusMessages *= 4;
233 }
234 }
235 }
236
237 abstract protected boolean eventMarksEndOfLife(E eventObject);
238
239 abstract protected void subAppend(CyclicBuffer<E> cb, E eventObject);
240
241
242
243
244
245
246
247
248 public boolean checkEntryConditions() {
249 if (!this.started) {
250 addError("Attempting to append to a non-started appender: " + this.getName());
251 return false;
252 }
253
254 if (this.eventEvaluator == null) {
255 addError("No EventEvaluator is set for appender [" + name + "].");
256 return false;
257 }
258
259 if (this.layout == null) {
260 addError("No layout set for appender named [" + name
261 + "]. For more information, please visit http://logback.qos.ch/codes.html#smtp_no_layout");
262 return false;
263 }
264 return true;
265 }
266
267 synchronized public void stop() {
268 this.started = false;
269 }
270
271 InternetAddress getAddress(String addressStr) {
272 try {
273 return new InternetAddress(addressStr);
274 } catch (AddressException e) {
275 addError("Could not parse address [" + addressStr + "].", e);
276 return null;
277 }
278 }
279
280 private List<InternetAddress> parseAddress(E event) {
281 int len = toPatternLayoutList.size();
282
283 List<InternetAddress> iaList = new ArrayList<InternetAddress>();
284
285 for (int i = 0; i < len; i++) {
286 try {
287 PatternLayoutBase<E> emailPL = toPatternLayoutList.get(i);
288 String emailAdrr = emailPL.doLayout(event);
289 if (emailAdrr == null || emailAdrr.length() == 0) {
290 continue;
291 }
292 InternetAddress[] tmp = InternetAddress.parse(emailAdrr, true);
293 iaList.addAll(Arrays.asList(tmp));
294 } catch (AddressException e) {
295 addError("Could not parse email address for [" + toPatternLayoutList.get(i) + "] for event [" + event
296 + "]", e);
297 return iaList;
298 }
299 }
300
301 return iaList;
302 }
303
304
305
306
307 public List<PatternLayoutBase<E>> getToList() {
308 return toPatternLayoutList;
309 }
310
311
312
313
314 protected void updateMimeMsg(MimeMessage mimeMsg, CyclicBuffer<E> cb, E lastEventObject) {
315 }
316
317
318
319
320 @SuppressWarnings("null")
321 protected void sendBuffer(CyclicBuffer<E> cb, E lastEventObject) {
322
323
324
325 try {
326 MimeBodyPart part = new MimeBodyPart();
327
328 StringBuffer sbuf = new StringBuffer();
329
330 String header = layout.getFileHeader();
331 if (header != null) {
332 sbuf.append(header);
333 }
334 String presentationHeader = layout.getPresentationHeader();
335 if (presentationHeader != null) {
336 sbuf.append(presentationHeader);
337 }
338 fillBuffer(cb, sbuf);
339 String presentationFooter = layout.getPresentationFooter();
340 if (presentationFooter != null) {
341 sbuf.append(presentationFooter);
342 }
343 String footer = layout.getFileFooter();
344 if (footer != null) {
345 sbuf.append(footer);
346 }
347
348 String subjectStr = "Undefined subject";
349 if (subjectLayout != null) {
350 subjectStr = subjectLayout.doLayout(lastEventObject);
351
352
353
354
355 int newLinePos = (subjectStr != null) ? subjectStr.indexOf('\n') : -1;
356 if (newLinePos > -1) {
357 subjectStr = subjectStr.substring(0, newLinePos);
358 }
359 }
360
361 MimeMessage mimeMsg = new MimeMessage(session);
362
363 if (from != null) {
364 mimeMsg.setFrom(getAddress(from));
365 } else {
366 mimeMsg.setFrom();
367 }
368
369 mimeMsg.setSubject(subjectStr, charsetEncoding);
370
371 List<InternetAddress> destinationAddresses = parseAddress(lastEventObject);
372 if (destinationAddresses.isEmpty()) {
373 addInfo("Empty destination address. Aborting email transmission");
374 return;
375 }
376
377 InternetAddress[] toAddressArray = destinationAddresses.toArray(EMPTY_IA_ARRAY);
378 mimeMsg.setRecipients(Message.RecipientType.TO, toAddressArray);
379
380 String contentType = layout.getContentType();
381
382 if (ContentTypeUtil.isTextual(contentType)) {
383 part.setText(sbuf.toString(), charsetEncoding, ContentTypeUtil.getSubType(contentType));
384 } else {
385 part.setContent(sbuf.toString(), layout.getContentType());
386 }
387
388 Multipart mp = new MimeMultipart();
389 mp.addBodyPart(part);
390 mimeMsg.setContent(mp);
391
392
393 updateMimeMsg(mimeMsg, cb, lastEventObject);
394
395 mimeMsg.setSentDate(new Date());
396 addInfo("About to send out SMTP message \"" + subjectStr + "\" to " + Arrays.toString(toAddressArray));
397 Transport.send(mimeMsg);
398 } catch (Exception e) {
399 addError("Error occurred while sending e-mail notification.", e);
400 }
401 }
402
403 abstract protected void fillBuffer(CyclicBuffer<E> cb, StringBuffer sbuf);
404
405
406
407
408 public String getFrom() {
409 return from;
410 }
411
412
413
414
415 public String getSubject() {
416 return subjectStr;
417 }
418
419
420
421
422
423 public void setFrom(String from) {
424 this.from = from;
425 }
426
427
428
429
430
431 public void setSubject(String subject) {
432 this.subjectStr = subject;
433 }
434
435
436
437
438
439
440 public void setSMTPHost(String smtpHost) {
441 setSmtpHost(smtpHost);
442 }
443
444
445
446
447
448 public void setSmtpHost(String smtpHost) {
449 this.smtpHost = smtpHost;
450 }
451
452
453
454
455 public String getSMTPHost() {
456 return getSmtpHost();
457 }
458
459
460
461
462 public String getSmtpHost() {
463 return smtpHost;
464 }
465
466
467
468
469
470
471 public void setSMTPPort(int port) {
472 setSmtpPort(port);
473 }
474
475
476
477
478
479
480 public void setSmtpPort(int port) {
481 this.smtpPort = port;
482 }
483
484
485
486
487
488
489 public int getSMTPPort() {
490 return getSmtpPort();
491 }
492
493
494
495
496
497
498 public int getSmtpPort() {
499 return smtpPort;
500 }
501
502 public String getLocalhost() {
503 return localhost;
504 }
505
506
507
508
509
510
511
512
513
514
515
516
517 public void setLocalhost(String localhost) {
518 this.localhost = localhost;
519 }
520
521 public CyclicBufferTracker<E> getCyclicBufferTracker() {
522 return cbTracker;
523 }
524
525 public void setCyclicBufferTracker(CyclicBufferTracker<E> cbTracker) {
526 this.cbTracker = cbTracker;
527 }
528
529 public Discriminator<E> getDiscriminator() {
530 return discriminator;
531 }
532
533 public void setDiscriminator(Discriminator<E> discriminator) {
534 this.discriminator = discriminator;
535 }
536
537 public boolean isAsynchronousSending() {
538 return asynchronousSending;
539 }
540
541
542
543
544
545
546
547
548
549 public void setAsynchronousSending(boolean asynchronousSending) {
550 this.asynchronousSending = asynchronousSending;
551 }
552
553 public void addTo(String to) {
554 if (to == null || to.length() == 0) {
555 throw new IllegalArgumentException("Null or empty <to> property");
556 }
557 PatternLayoutBase<E> plb = makeNewToPatternLayout(to.trim());
558 plb.setContext(context);
559 plb.start();
560 this.toPatternLayoutList.add(plb);
561 }
562
563 abstract protected PatternLayoutBase<E> makeNewToPatternLayout(String toPattern);
564
565 public List<String> getToAsListOfString() {
566 List<String> toList = new ArrayList<String>();
567 for (PatternLayoutBase<E> plb : toPatternLayoutList) {
568 toList.add(plb.getPattern());
569 }
570 return toList;
571 }
572
573 public boolean isSTARTTLS() {
574 return starttls;
575 }
576
577 public void setSTARTTLS(boolean startTLS) {
578 this.starttls = startTLS;
579 }
580
581 public boolean isSSL() {
582 return ssl;
583 }
584
585 public void setSSL(boolean ssl) {
586 this.ssl = ssl;
587 }
588
589
590
591
592
593
594
595 public void setEvaluator(EventEvaluator<E> eventEvaluator) {
596 this.eventEvaluator = eventEvaluator;
597 }
598
599 public String getUsername() {
600 return username;
601 }
602
603 public void setUsername(String username) {
604 this.username = username;
605 }
606
607 public String getPassword() {
608 return password;
609 }
610
611 public void setPassword(String password) {
612 this.password = password;
613 }
614
615
616
617
618
619 public String getCharsetEncoding() {
620 return charsetEncoding;
621 }
622
623 public String getJndiLocation() {
624 return jndiLocation;
625 }
626
627
628
629
630
631
632
633
634 public void setJndiLocation(String jndiLocation) {
635 this.jndiLocation = jndiLocation;
636 }
637
638 public boolean isSessionViaJNDI() {
639 return sessionViaJNDI;
640 }
641
642
643
644
645
646
647
648
649 public void setSessionViaJNDI(boolean sessionViaJNDI) {
650 this.sessionViaJNDI = sessionViaJNDI;
651 }
652
653
654
655
656
657
658
659 public void setCharsetEncoding(String charsetEncoding) {
660 this.charsetEncoding = charsetEncoding;
661 }
662
663 public Layout<E> getLayout() {
664 return layout;
665 }
666
667 public void setLayout(Layout<E> layout) {
668 this.layout = layout;
669 }
670
671 class SenderRunnable implements Runnable {
672
673 final CyclicBuffer<E> cyclicBuffer;
674 final E e;
675
676 SenderRunnable(CyclicBuffer<E> cyclicBuffer, E e) {
677 this.cyclicBuffer = cyclicBuffer;
678 this.e = e;
679 }
680
681 public void run() {
682 sendBuffer(cyclicBuffer, e);
683 }
684 }
685 }