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