1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package org.mycore.common;
20
21 import java.io.IOException;
22 import java.net.URL;
23 import java.util.ArrayList;
24 import java.util.Collections;
25 import java.util.Date;
26 import java.util.List;
27 import java.util.Map;
28 import java.util.Optional;
29 import java.util.Properties;
30 import java.util.stream.Collectors;
31
32 import org.apache.logging.log4j.LogManager;
33 import org.apache.logging.log4j.Logger;
34 import org.jdom2.Document;
35 import org.jdom2.Element;
36 import org.jdom2.JDOMException;
37 import org.jdom2.output.Format;
38 import org.jdom2.output.XMLOutputter;
39 import org.jdom2.transform.JDOMSource;
40 import org.mycore.common.MCRMailer.EMail.MessagePart;
41 import org.mycore.common.config.MCRConfiguration2;
42 import org.mycore.common.config.MCRConfigurationException;
43 import org.mycore.common.content.MCRContent;
44 import org.mycore.common.content.MCRJAXBContent;
45 import org.mycore.common.content.MCRJDOMContent;
46 import org.mycore.common.content.transformer.MCRXSL2XMLTransformer;
47 import org.mycore.common.xsl.MCRParameterCollector;
48 import org.mycore.frontend.servlets.MCRServlet;
49 import org.mycore.frontend.servlets.MCRServletJob;
50 import org.xml.sax.SAXParseException;
51
52 import jakarta.activation.DataHandler;
53 import jakarta.activation.DataSource;
54 import jakarta.activation.URLDataSource;
55 import jakarta.mail.Authenticator;
56 import jakarta.mail.Message;
57 import jakarta.mail.Multipart;
58 import jakarta.mail.PasswordAuthentication;
59 import jakarta.mail.Session;
60 import jakarta.mail.Transport;
61 import jakarta.mail.internet.InternetAddress;
62 import jakarta.mail.internet.MimeBodyPart;
63 import jakarta.mail.internet.MimeMessage;
64 import jakarta.mail.internet.MimeMultipart;
65 import jakarta.xml.bind.JAXBContext;
66 import jakarta.xml.bind.JAXBException;
67 import jakarta.xml.bind.Unmarshaller;
68 import jakarta.xml.bind.annotation.XmlAttribute;
69 import jakarta.xml.bind.annotation.XmlElement;
70 import jakarta.xml.bind.annotation.XmlEnum;
71 import jakarta.xml.bind.annotation.XmlEnumValue;
72 import jakarta.xml.bind.annotation.XmlRootElement;
73 import jakarta.xml.bind.annotation.XmlType;
74 import jakarta.xml.bind.annotation.XmlValue;
75
76
77
78
79
80
81
82
83
84
85
86 public class MCRMailer extends MCRServlet {
87
88 private static final Logger LOGGER = LogManager.getLogger(MCRMailer.class);
89
90 private static final String DELIMITER = "\n--------------------------------------\n";
91
92 private static Session mailSession;
93
94 protected static final String ENCODING;
95
96
97 protected static int numTries;
98
99 private static final long serialVersionUID = 1L;
100
101 @Override
102 protected void doGetPost(MCRServletJob job) throws Exception {
103 String goTo = job.getRequest().getParameter("goto");
104 String xsl = job.getRequest().getParameter("xsl");
105
106 Document input = (Document) (job.getRequest().getAttribute("MCRXEditorSubmission"));
107 MCRMailer.sendMail(input, xsl);
108
109 job.getResponse().sendRedirect(goTo);
110 }
111
112 static {
113 ENCODING = MCRConfiguration2.getStringOrThrow("MCR.Mail.Encoding");
114
115 Properties mailProperties = new Properties();
116
117 try {
118 Authenticator auth = null;
119
120 numTries = MCRConfiguration2.getOrThrow("MCR.Mail.NumTries", Integer::parseInt);
121 if (MCRConfiguration2.getString("MCR.Mail.User").isPresent()
122 && MCRConfiguration2.getString("MCR.Mail.Password").isPresent()) {
123 auth = new SMTPAuthenticator();
124 mailProperties.setProperty("mail.smtp.auth", "true");
125 }
126 String starttsl = MCRConfiguration2.getString("MCR.Mail.STARTTLS").orElse("disabled");
127 if ("enabled".equals(starttsl)) {
128 mailProperties.setProperty("mail.smtp.starttls.enabled", "true");
129 } else if ("required".equals(starttsl)) {
130 mailProperties.setProperty("mail.smtp.starttls.enabled", "true");
131 mailProperties.setProperty("mail.smtp.starttls.required", "true");
132 }
133 mailProperties.setProperty("mail.smtp.host", MCRConfiguration2.getStringOrThrow("MCR.Mail.Server"));
134 mailProperties.setProperty("mail.transport.protocol",
135 MCRConfiguration2.getStringOrThrow("MCR.Mail.Protocol"));
136 mailProperties.setProperty("mail.smtp.port", MCRConfiguration2.getString("MCR.Mail.Port").orElse("25"));
137 mailSession = Session.getDefaultInstance(mailProperties, auth);
138 mailSession.setDebug(MCRConfiguration2.getOrThrow("MCR.Mail.Debug", Boolean::parseBoolean));
139 } catch (MCRConfigurationException mcrx) {
140 String msg = "Missing e-mail configuration data.";
141 LOGGER.fatal(msg, mcrx);
142 }
143 }
144
145
146
147
148
149
150
151
152
153
154
155
156
157 public static void send(String sender, String recipient, String subject, String body) {
158 LOGGER.debug("Called plaintext send method with single recipient.");
159
160 ArrayList<String> recipients = new ArrayList<>();
161 recipients.add(recipient);
162 send(sender, null, recipients, null, subject, body, null);
163 }
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180 public static void send(String sender, List<String> recipients, String subject, String body, boolean bcc) {
181 LOGGER.debug("Called plaintext send method with multiple recipients.");
182
183 List<String> bccList = null;
184
185 if (bcc) {
186 bccList = new ArrayList<>();
187 bccList.add(sender);
188 }
189
190 send(sender, null, recipients, bccList, subject, body, null);
191 }
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207 public static void send(String sender, String recipient, String subject, String body, List<String> parts) {
208 LOGGER.debug("Called multipart send method with single recipient.");
209
210 ArrayList<String> recipients = new ArrayList<>();
211 recipients.add(recipient);
212 send(sender, null, recipients, null, subject, body, parts);
213 }
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232 public static void send(String sender, List<String> recipients, String subject, String body, List<String> parts,
233 boolean bcc) {
234 LOGGER.debug("Called multipart send method with multiple recipients.");
235
236 List<String> bccList = null;
237
238 if (bcc) {
239 bccList = new ArrayList<>();
240 bccList.add(sender);
241 }
242
243 send(sender, null, recipients, bccList, subject, body, parts);
244 }
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261 public static void send(Element email) {
262 try {
263 send(email, false);
264 } catch (Exception e) {
265 LOGGER.error(e.getMessage());
266 }
267 }
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286 public static void send(Element email, Boolean allowException) throws Exception {
287 EMail mail = EMail.parseXML(email);
288
289 if (allowException) {
290 if (mail.to == null || mail.to.isEmpty()) {
291 throw new MCRException("No receiver defined for mail\n" + mail + '\n');
292 }
293
294 trySending(mail);
295 } else {
296 send(mail);
297 }
298 }
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323 public static void send(final String from, final List<String> replyTo, final List<String> to,
324 final List<String> bcc, final String subject, final String body, final List<String> parts) {
325 EMail mail = new EMail();
326
327 mail.from = from;
328 mail.replyTo = replyTo;
329 mail.to = to;
330 mail.bcc = bcc;
331 mail.subject = subject;
332
333 mail.msgParts = new ArrayList<>();
334 mail.msgParts.add(new MessagePart(body));
335
336 mail.parts = parts;
337
338 send(mail);
339 }
340
341
342
343
344
345
346
347
348 public static void send(EMail mail) {
349 if (mail.to == null || mail.to.isEmpty()) {
350 throw new MCRException("No receiver defined for mail\n" + mail + '\n');
351 }
352
353 try {
354 if (numTries > 0) {
355 trySending(mail);
356 }
357 } catch (Exception ex) {
358 LOGGER.info("Sending e-mail failed: ", ex);
359 if (numTries < 2) {
360 return;
361 }
362
363 Thread t = new Thread(() -> {
364 for (int i = numTries - 1; i > 0; i--) {
365 LOGGER.info("Retrying in 5 minutes...");
366 try {
367 Thread.sleep(300000);
368 } catch (InterruptedException ignored) {
369 }
370
371 try {
372 trySending(mail);
373 LOGGER.info("Successfully resended e-mail.");
374 break;
375 } catch (Exception ex1) {
376 LOGGER.info("Sending e-mail failed: ", ex1);
377 }
378 }
379 });
380 t.start();
381 }
382 }
383
384 private static void trySending(EMail mail) throws Exception {
385 MimeMessage msg = new MimeMessage(mailSession);
386 msg.setFrom(EMail.buildAddress(mail.from));
387
388 Optional<List<InternetAddress>> toList = EMail.buildAddressList(mail.to);
389 if (toList.isPresent()) {
390 msg.addRecipients(Message.RecipientType.TO, toList.get().toArray(new InternetAddress[toList.get().size()]));
391 }
392
393 Optional<List<InternetAddress>> replyToList = EMail.buildAddressList(mail.replyTo);
394 if (replyToList.isPresent()) {
395 msg.setReplyTo((replyToList.get().toArray(new InternetAddress[replyToList.get().size()])));
396 }
397
398 Optional<List<InternetAddress>> bccList = EMail.buildAddressList(mail.bcc);
399 if (bccList.isPresent()) {
400 msg.addRecipients(Message.RecipientType.BCC,
401 bccList.get().toArray(new InternetAddress[bccList.get().size()]));
402 }
403
404 msg.setSentDate(new Date());
405 msg.setSubject(mail.subject, ENCODING);
406
407 if (mail.parts != null && !mail.parts.isEmpty() || mail.msgParts != null && mail.msgParts.size() > 1) {
408 Multipart multipart = new MimeMultipart();
409
410 MimeBodyPart messagePart = new MimeBodyPart();
411
412 if (mail.msgParts.size() > 1) {
413 multipart = new MimeMultipart("mixed");
414 MimeMultipart alternative = new MimeMultipart("alternative");
415
416 for (MessagePart m : mail.msgParts) {
417 messagePart = new MimeBodyPart();
418 messagePart.setText(m.message, ENCODING, m.type.value());
419 alternative.addBodyPart(messagePart);
420 }
421
422 messagePart = new MimeBodyPart();
423 messagePart.setContent(alternative);
424 multipart.addBodyPart(messagePart);
425 } else {
426 Optional<MessagePart> plainMsg = mail.getTextMessage();
427 if (plainMsg.isPresent()) {
428 messagePart.setText(plainMsg.get().message, ENCODING);
429 multipart.addBodyPart(messagePart);
430 }
431 }
432
433 if (mail.parts != null && !mail.parts.isEmpty()) {
434 for (String part : mail.parts) {
435 messagePart = new MimeBodyPart();
436
437 URL url = new URL(part);
438 DataSource source = new URLDataSource(url);
439 messagePart.setDataHandler(new DataHandler(source));
440
441 String fileName = url.getPath();
442 if (fileName.contains("\\")) {
443 fileName = fileName.substring(fileName.lastIndexOf("\\") + 1);
444 } else if (fileName.contains("/")) {
445 fileName = fileName.substring(fileName.lastIndexOf("/") + 1);
446 }
447 messagePart.setFileName(fileName);
448
449 multipart.addBodyPart(messagePart);
450 }
451 }
452
453 msg.setContent(multipart);
454 } else {
455 Optional<MessagePart> plainMsg = mail.getTextMessage();
456 if (plainMsg.isPresent()) {
457 msg.setText(plainMsg.get().message, ENCODING);
458 }
459 }
460
461 LOGGER.info("Sending e-mail to {}", mail.to);
462 Transport.send(msg);
463 }
464
465
466
467
468
469
470
471
472
473
474
475
476 public static Element sendMail(Document input, String stylesheet, Map<String, String> parameters) throws Exception {
477 LOGGER.info("Generating e-mail from {} using {}.xsl", input.getRootElement().getName(), stylesheet);
478 if (LOGGER.isDebugEnabled()) {
479 debug(input.getRootElement());
480 }
481
482 Element eMail = transform(input, stylesheet, parameters).getRootElement();
483 if (LOGGER.isDebugEnabled()) {
484 debug(eMail);
485 }
486
487 if (eMail.getChildren("to").isEmpty()) {
488 LOGGER.warn("Will not send e-mail, no 'to' address specified");
489 } else {
490 LOGGER.info("Sending e-mail to {}: {}", eMail.getChildText("to"), eMail.getChildText("subject"));
491 MCRMailer.send(eMail);
492 }
493
494 return eMail;
495 }
496
497
498
499
500
501
502
503
504
505
506
507 public static Element sendMail(Document input, String stylesheet) throws Exception {
508 return sendMail(input, stylesheet, Collections.emptyMap());
509 }
510
511
512
513
514
515
516
517
518
519 private static Document transform(Document input, String stylesheet, Map<String, String> parameters)
520 throws Exception {
521 MCRJDOMContent source = new MCRJDOMContent(input);
522 MCRXSL2XMLTransformer transformer = MCRXSL2XMLTransformer.getInstance("xsl/" + stylesheet + ".xsl");
523 MCRParameterCollector parameterCollector = MCRParameterCollector.getInstanceFromUserSession();
524 parameterCollector.setParameters(parameters);
525 MCRContent result = transformer.transform(source, parameterCollector);
526 return result.asXML();
527 }
528
529
530 private static void debug(Element xml) {
531 XMLOutputter xout = new XMLOutputter(Format.getPrettyFormat());
532 LOGGER.debug(DELIMITER + "{}" + DELIMITER, xout.outputString(xml));
533 }
534
535 @XmlRootElement(name = "email")
536 public static class EMail {
537
538 private static final JAXBContext JAXB_CONTEXT = initContext();
539
540 @XmlElement
541 public String from;
542
543 @XmlElement
544 public List<String> replyTo;
545
546 @XmlElement
547 public List<String> to;
548
549 @XmlElement
550 public List<String> bcc;
551
552 @XmlElement
553 public String subject;
554
555 @XmlElement(name = "body")
556 public List<MessagePart> msgParts;
557
558 @XmlElement(name = "part")
559 public List<String> parts;
560
561 private static JAXBContext initContext() {
562 try {
563 return JAXBContext.newInstance(EMail.class.getPackage().getName(), EMail.class.getClassLoader());
564 } catch (final JAXBException e) {
565 throw new MCRException("Could not instantiate JAXBContext.", e);
566 }
567 }
568
569
570
571
572
573
574
575 public static EMail parseXML(final Element xml) {
576 try {
577 final Unmarshaller unmarshaller = JAXB_CONTEXT.createUnmarshaller();
578 return (EMail) unmarshaller.unmarshal(new JDOMSource(xml));
579 } catch (final JAXBException e) {
580 throw new MCRException("Exception while transforming Element to EMail.", e);
581 }
582 }
583
584
585
586
587
588
589
590
591
592
593 private static InternetAddress buildAddress(String s) throws Exception {
594 if (!s.endsWith(">")) {
595 return new InternetAddress(s.trim());
596 }
597
598 String name = s.substring(0, s.lastIndexOf("<")).trim();
599 String addr = s.substring(s.lastIndexOf("<") + 1, s.length() - 1).trim();
600
601 if (name.startsWith("\"") && name.endsWith("\"")) {
602 name = name.substring(1, name.length() - 1);
603 }
604
605 return new InternetAddress(addr, name);
606 }
607
608
609
610
611
612
613
614
615 private static Optional<List<InternetAddress>> buildAddressList(final List<String> addresses) {
616 return addresses != null ? Optional.ofNullable(addresses.stream().map(address -> {
617 try {
618 return buildAddress(address);
619 } catch (Exception ex) {
620 return null;
621 }
622 }).collect(Collectors.toList())) : Optional.empty();
623 }
624
625
626
627
628
629
630 public Optional<MessagePart> getTextMessage() {
631 return msgParts != null ? Optional.ofNullable(msgParts).get().stream()
632 .filter(m -> m.type.equals(MessageType.TEXT)).findFirst() : Optional.empty();
633 }
634
635
636
637
638
639
640 public Optional<MessagePart> getHTMLMessage() {
641 return msgParts != null ? Optional.ofNullable(msgParts).get().stream()
642 .filter(m -> m.type.equals(MessageType.HTML)).findFirst() : Optional.empty();
643 }
644
645
646
647
648
649
650 public Document toXML() {
651 final MCRJAXBContent<EMail> content = new MCRJAXBContent<>(JAXB_CONTEXT, this);
652 try {
653 return content.asXML();
654 } catch (final SAXParseException | JDOMException | IOException e) {
655 throw new MCRException("Exception while transforming EMail to JDOM document.", e);
656 }
657 }
658
659
660
661
662 @Override
663 public String toString() {
664 final int maxLen = 10;
665 StringBuilder builder = new StringBuilder();
666 builder.append("EMail [");
667 if (from != null) {
668 builder.append("from=");
669 builder.append(from);
670 builder.append(", ");
671 }
672 if (replyTo != null) {
673 builder.append("replyTo=");
674 builder.append(replyTo.subList(0, Math.min(replyTo.size(), maxLen)));
675 builder.append(", ");
676 }
677 if (to != null) {
678 builder.append("to=");
679 builder.append(to.subList(0, Math.min(to.size(), maxLen)));
680 builder.append(", ");
681 }
682 if (bcc != null) {
683 builder.append("bcc=");
684 builder.append(bcc.subList(0, Math.min(bcc.size(), maxLen)));
685 builder.append(", ");
686 }
687 if (subject != null) {
688 builder.append("subject=");
689 builder.append(subject);
690 builder.append(", ");
691 }
692 if (msgParts != null) {
693 builder.append("msgParts=");
694 builder.append(msgParts.subList(0, Math.min(msgParts.size(), maxLen)));
695 builder.append(", ");
696 }
697 if (parts != null) {
698 builder.append("parts=");
699 builder.append(parts.subList(0, Math.min(parts.size(), maxLen)));
700 }
701 builder.append("]");
702 return builder.toString();
703 }
704
705 @XmlRootElement(name = "body")
706 public static class MessagePart {
707
708 @XmlAttribute
709 public MessageType type = MessageType.TEXT;
710
711 @XmlValue
712 public String message;
713
714 MessagePart() {
715 }
716
717 public MessagePart(final String message) {
718 this.message = message;
719 }
720
721 public MessagePart(final String message, final MessageType type) {
722 this.message = message;
723 this.type = type;
724 }
725
726
727
728
729 @Override
730 public String toString() {
731 final int maxLen = 50;
732 StringBuilder builder = new StringBuilder();
733 builder.append("MessagePart [");
734 if (type != null) {
735 builder.append("type=");
736 builder.append(type);
737 builder.append(", ");
738 }
739 if (message != null) {
740 builder.append("message=");
741 builder.append(message, 0, Math.min(message.length(), maxLen));
742 }
743 builder.append("]");
744 return builder.toString();
745 }
746 }
747
748 @XmlType(name = "mcrmailer-messagetype")
749 @XmlEnum
750 public enum MessageType {
751 @XmlEnumValue("text")
752 TEXT("text"),
753
754 @XmlEnumValue("html")
755 HTML("html");
756
757 private final String value;
758
759 MessageType(String v) {
760 value = v;
761 }
762
763 public String value() {
764 return value;
765 }
766
767 public static MessageType fromValue(String v) {
768 for (MessageType t : MessageType.values()) {
769 if (t.value.equals(v)) {
770 return t;
771 }
772 }
773 throw new IllegalArgumentException(v);
774 }
775 }
776 }
777
778 private static class SMTPAuthenticator extends jakarta.mail.Authenticator {
779
780 public PasswordAuthentication getPasswordAuthentication() {
781 return new PasswordAuthentication(MCRConfiguration2.getStringOrThrow("MCR.Mail.User"),
782 MCRConfiguration2.getStringOrThrow("MCR.Mail.Password"));
783 }
784 }
785 }