001 /*
002 * CDDL HEADER START
003 *
004 * The contents of this file are subject to the terms of the
005 * Common Development and Distribution License, Version 1.0 only
006 * (the "License"). You may not use this file except in compliance
007 * with the License.
008 *
009 * You can obtain a copy of the license at
010 * trunk/opends/resource/legal-notices/OpenDS.LICENSE
011 * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
012 * See the License for the specific language governing permissions
013 * and limitations under the License.
014 *
015 * When distributing Covered Code, include this CDDL HEADER in each
016 * file and include the License file at
017 * trunk/opends/resource/legal-notices/OpenDS.LICENSE. If applicable,
018 * add the following below this CDDL HEADER, with the fields enclosed
019 * by brackets "[]" replaced with your own identifying information:
020 * Portions Copyright [yyyy] [name of copyright owner]
021 *
022 * CDDL HEADER END
023 *
024 *
025 * Copyright 2008 Sun Microsystems, Inc.
026 */
027 package org.opends.server.extensions;
028
029
030
031 import java.io.BufferedReader;
032 import java.io.File;
033 import java.io.FileReader;
034 import java.util.HashMap;
035 import java.util.LinkedList;
036 import java.util.List;
037 import java.util.Properties;
038 import java.util.Set;
039
040 import org.opends.messages.Message;
041 import org.opends.messages.MessageBuilder;
042 import org.opends.server.admin.server.ConfigurationChangeListener;
043 import org.opends.server.admin.std.server.AccountStatusNotificationHandlerCfg;
044 import org.opends.server.admin.std.server.
045 SMTPAccountStatusNotificationHandlerCfg;
046 import org.opends.server.api.AccountStatusNotificationHandler;
047 import org.opends.server.config.ConfigException;
048 import org.opends.server.core.DirectoryServer;
049 import org.opends.server.loggers.debug.DebugTracer;
050 import org.opends.server.types.AccountStatusNotification;
051 import org.opends.server.types.AccountStatusNotificationProperty;
052 import org.opends.server.types.AccountStatusNotificationType;
053 import org.opends.server.types.Attribute;
054 import org.opends.server.types.AttributeType;
055 import org.opends.server.types.AttributeValue;
056 import org.opends.server.types.ConfigChangeResult;
057 import org.opends.server.types.DebugLogLevel;
058 import org.opends.server.types.Entry;
059 import org.opends.server.types.InitializationException;
060 import org.opends.server.types.ResultCode;
061 import org.opends.server.util.EMailMessage;
062
063 import static org.opends.messages.ExtensionMessages.*;
064 import static org.opends.server.loggers.ErrorLogger.*;
065 import static org.opends.server.loggers.debug.DebugLogger.*;
066 import static org.opends.server.util.StaticUtils.*;
067
068
069
070 /**
071 * This class provides an implementation of an account status notification
072 * handler that can send e-mail messages via SMTP to end users and/or
073 * administrators whenever an account status notification occurs. The e-mail
074 * messages will be generated from template files, which contain the information
075 * to use to create the message body. The template files may contain plain
076 * text, in addition to the following tokens:
077 * <UL>
078 * <LI>%%notification-type%% -- Will be replaced with the name of the
079 * account status notification type for the notification.</LI>
080 * <LI>%%notification-message%% -- Will be replaced with the message for the
081 * account status notification.</LI>
082 * <LI>%%notification-user-dn%% -- Will be replaced with the string
083 * representation of the DN for the user that is the target of the
084 * account status notification.</LI>
085 * <LI>%%notification-user-attr:attrname%% -- Will be replaced with the value
086 * of the attribute specified by attrname from the user's entry. If the
087 * specified attribute has multiple values, then the first value
088 * encountered will be used. If the specified attribute does not have any
089 * values, then it will be replaced with an emtpy string.</LI>
090 * <LI>%%notification-property:propname%% -- Will be replaced with the value
091 * of the specified notification property from the account status
092 * notification. If the specified property has multiple values, then the
093 * first value encountered will be used. If the specified property does
094 * not have any values, then it will be replaced with an emtpy
095 * string.</LI>
096 * </UL>
097 */
098 public class SMTPAccountStatusNotificationHandler
099 extends AccountStatusNotificationHandler
100 <SMTPAccountStatusNotificationHandlerCfg>
101 implements ConfigurationChangeListener
102 <SMTPAccountStatusNotificationHandlerCfg>
103 {
104 /**
105 * The tracer object for the debug logger.
106 */
107 private static final DebugTracer TRACER = getTracer();
108
109
110
111 // A mapping between the notification types and the message template.
112 private HashMap<AccountStatusNotificationType,
113 List<NotificationMessageTemplateElement>> templateMap;
114
115 // A mapping between the notification types and the message subject.
116 private HashMap<AccountStatusNotificationType,String> subjectMap;
117
118 // The current configuration for this account status notification handler.
119 private SMTPAccountStatusNotificationHandlerCfg currentConfig;
120
121
122
123 /**
124 * Creates a new, uninitialized instance of this account status notification
125 * handler.
126 */
127 public SMTPAccountStatusNotificationHandler()
128 {
129 super();
130 }
131
132
133
134 /**
135 * {@inheritDoc}
136 */
137 public void initializeStatusNotificationHandler(
138 SMTPAccountStatusNotificationHandlerCfg configuration)
139 throws ConfigException, InitializationException
140 {
141 currentConfig = configuration;
142 currentConfig.addSMTPChangeListener(this);
143
144 subjectMap = parseSubjects(configuration);
145 templateMap = parseTemplates(configuration);
146
147 // Make sure that the Directory Server is configured with information about
148 // one or more mail servers.
149 List<Properties> propList = DirectoryServer.getMailServerPropertySets();
150 if ((propList == null) || propList.isEmpty())
151 {
152 throw new ConfigException(ERR_SMTP_ASNH_NO_MAIL_SERVERS_CONFIGURED.get(
153 configuration.dn().toString()));
154 }
155
156 // Make sure that either an explicit recipient list or a set of email
157 // address attributes were provided.
158 Set<AttributeType> mailAttrs = configuration.getEmailAddressAttributeType();
159 Set<String> recipients = configuration.getRecipientAddress();
160 if (((mailAttrs == null) || mailAttrs.isEmpty()) &&
161 ((recipients == null) || recipients.isEmpty()))
162 {
163 throw new ConfigException(ERR_SMTP_ASNH_NO_RECIPIENTS.get(
164 configuration.dn().toString()));
165 }
166 }
167
168
169
170 /**
171 * Examines the provided configuration and parses the message subject
172 * information from it.
173 *
174 * @param configuration The configuration to be examined.
175 *
176 * @return A mapping between the account status notification type and the
177 * subject that should be used for messages generated for
178 * notifications with that type.
179 *
180 * @throws ConfigException If a problem occurs while parsing the subject
181 * configuration.
182 */
183 private HashMap<AccountStatusNotificationType,String> parseSubjects(
184 SMTPAccountStatusNotificationHandlerCfg configuration)
185 throws ConfigException
186 {
187 HashMap<AccountStatusNotificationType,String> map =
188 new HashMap<AccountStatusNotificationType,String>();
189
190 for (String s : configuration.getMessageSubject())
191 {
192 int colonPos = s.indexOf(':');
193 if (colonPos < 0)
194 {
195 throw new ConfigException(ERR_SMTP_ASNH_SUBJECT_NO_COLON.get(s,
196 configuration.dn().toString()));
197 }
198
199 String notificationTypeName = s.substring(0, colonPos).trim();
200 AccountStatusNotificationType t =
201 AccountStatusNotificationType.typeForName(notificationTypeName);
202 if (t == null)
203 {
204 throw new ConfigException(
205 ERR_SMTP_ASNH_SUBJECT_INVALID_NOTIFICATION_TYPE.get(
206 s, configuration.dn().toString(),
207 notificationTypeName));
208 }
209 else if (map.containsKey(t))
210 {
211 throw new ConfigException(ERR_SMTP_ASNH_SUBJECT_DUPLICATE_TYPE.get(
212 configuration.dn().toString(),
213 notificationTypeName));
214 }
215
216 map.put(t, s.substring(colonPos+1).trim());
217 if (debugEnabled())
218 {
219 TRACER.debugInfo("Subject for notification type " + t.getName() +
220 ": " + map.get(t));
221 }
222 }
223
224 return map;
225 }
226
227
228
229 /**
230 * Examines the provided configuration and parses the message template
231 * information from it.
232 *
233 * @param configuration The configuration to be examined.
234 *
235 * @return A mapping between the account status notification type and the
236 * template that should be used to generate messages for
237 * notifications with that type.
238 *
239 * @throws ConfigException If a problem occurs while parsing the template
240 * configuration.
241 */
242 private HashMap<AccountStatusNotificationType,
243 List<NotificationMessageTemplateElement>> parseTemplates(
244 SMTPAccountStatusNotificationHandlerCfg configuration)
245 throws ConfigException
246 {
247 HashMap<AccountStatusNotificationType,
248 List<NotificationMessageTemplateElement>> map =
249 new HashMap<AccountStatusNotificationType,
250 List<NotificationMessageTemplateElement>>();
251
252 for (String s : configuration.getMessageTemplateFile())
253 {
254 int colonPos = s.indexOf(':');
255 if (colonPos < 0)
256 {
257 throw new ConfigException(ERR_SMTP_ASNH_TEMPLATE_NO_COLON.get(s,
258 configuration.dn().toString()));
259 }
260
261 String notificationTypeName = s.substring(0, colonPos).trim();
262 AccountStatusNotificationType t =
263 AccountStatusNotificationType.typeForName(notificationTypeName);
264 if (t == null)
265 {
266 throw new ConfigException(
267 ERR_SMTP_ASNH_TEMPLATE_INVALID_NOTIFICATION_TYPE.get(
268 s, configuration.dn().toString(),
269 notificationTypeName));
270 }
271 else if (map.containsKey(t))
272 {
273 throw new ConfigException(ERR_SMTP_ASNH_TEMPLATE_DUPLICATE_TYPE.get(
274 configuration.dn().toString(),
275 notificationTypeName));
276 }
277
278 String path = s.substring(colonPos+1).trim();
279 File f = new File(path);
280 if (! f.isAbsolute() )
281 {
282 f = new File(DirectoryServer.getServerRoot() + File.separator +
283 path);
284 }
285 if (! f.exists())
286 {
287 throw new ConfigException(ERR_SMTP_ASNH_TEMPLATE_NO_SUCH_FILE.get(
288 path, configuration.dn().toString()));
289 }
290
291 map.put(t, parseTemplateFile(f));
292 if (debugEnabled())
293 {
294 TRACER.debugInfo("Decoded template elment list for type " +
295 t.getName());
296 }
297 }
298
299 return map;
300 }
301
302
303
304 /**
305 * Parses the specified template file into a list of notification message
306 * template elements.
307 *
308 * @param f A reference to the template file to be parsed.
309 *
310 * @return A list of notification message template elements parsed from the
311 * specified file.
312 *
313 * @throws ConfigException If error occurs while attempting to parse the
314 * template file.
315 */
316 private List<NotificationMessageTemplateElement> parseTemplateFile(File f)
317 throws ConfigException
318 {
319 LinkedList<NotificationMessageTemplateElement> elementList =
320 new LinkedList<NotificationMessageTemplateElement>();
321
322 BufferedReader reader = null;
323 try
324 {
325 reader = new BufferedReader(new FileReader(f));
326 int lineNumber = 0;
327 while (true)
328 {
329 String line = reader.readLine();
330 if (line == null)
331 {
332 break;
333 }
334
335 if (debugEnabled())
336 {
337 TRACER.debugInfo("Read message template line " + line);
338 }
339
340 lineNumber++;
341 int startPos = 0;
342 while (startPos < line.length())
343 {
344 int delimPos = line.indexOf("%%", startPos);
345 if (delimPos < 0)
346 {
347 if (debugEnabled())
348 {
349 TRACER.debugInfo("No more tokens -- adding text " +
350 line.substring(startPos));
351 }
352
353 elementList.add(new TextNotificationMessageTemplateElement(
354 line.substring(startPos)));
355 break;
356 }
357 else
358 {
359 if (delimPos > startPos)
360 {
361 if (debugEnabled())
362 {
363 TRACER.debugInfo("Adding text before token " +
364 line.substring(startPos));
365 }
366
367 elementList.add(new TextNotificationMessageTemplateElement(
368 line.substring(startPos, delimPos)));
369 }
370
371 int closeDelimPos = line.indexOf("%%", delimPos+1);
372 if (closeDelimPos < 0)
373 {
374 // There was an opening %% but not a closing one.
375 throw new ConfigException(
376 ERR_SMTP_ASNH_TEMPLATE_UNCLOSED_TOKEN.get(
377 delimPos, lineNumber));
378 }
379 else
380 {
381 String tokenStr = line.substring(delimPos+2, closeDelimPos);
382 String lowerTokenStr = toLowerCase(tokenStr);
383 if (lowerTokenStr.equals("notification-type"))
384 {
385 if (debugEnabled())
386 {
387 TRACER.debugInfo("Found a notification type token " +
388 tokenStr);
389 }
390
391 elementList.add(
392 new NotificationTypeNotificationMessageTemplateElement());
393 }
394 else if (lowerTokenStr.equals("notification-message"))
395 {
396 if (debugEnabled())
397 {
398 TRACER.debugInfo("Found a notification message token " +
399 tokenStr);
400 }
401
402 elementList.add(
403 new NotificationMessageNotificationMessageTemplateElement());
404 }
405 else if (lowerTokenStr.equals("notification-user-dn"))
406 {
407 if (debugEnabled())
408 {
409 TRACER.debugInfo("Found a notification user DN token " +
410 tokenStr);
411 }
412
413 elementList.add(
414 new UserDNNotificationMessageTemplateElement());
415 }
416 else if (lowerTokenStr.startsWith("notification-user-attr:"))
417 {
418 String attrName = lowerTokenStr.substring(23);
419 AttributeType attrType =
420 DirectoryServer.getAttributeType(attrName, false);
421 if (attrType == null)
422 {
423 throw new ConfigException(
424 ERR_SMTP_ASNH_TEMPLATE_UNDEFINED_ATTR_TYPE.get(
425 delimPos, lineNumber, attrName));
426 }
427 else
428 {
429 if (debugEnabled())
430 {
431 TRACER.debugInfo("Found a user attribute token for " +
432 attrType.getNameOrOID() + " -- " +
433 tokenStr);
434 }
435
436 elementList.add(
437 new UserAttributeNotificationMessageTemplateElement(
438 attrType));
439 }
440 }
441 else if (lowerTokenStr.startsWith("notification-property:"))
442 {
443 String propertyName = lowerTokenStr.substring(22);
444 AccountStatusNotificationProperty property =
445 AccountStatusNotificationProperty.forName(propertyName);
446 if (property == null)
447 {
448 throw new ConfigException(
449 ERR_SMTP_ASNH_TEMPLATE_UNDEFINED_PROPERTY.get(
450 delimPos, lineNumber, propertyName));
451 }
452 else
453 {
454 if (debugEnabled())
455 {
456 TRACER.debugInfo("Found a notification property token " +
457 "for " + propertyName + " -- " + tokenStr);
458 }
459
460 elementList.add(
461 new NotificationPropertyNotificationMessageTemplateElement(
462 property));
463 }
464 }
465 else
466 {
467 throw new ConfigException(
468 ERR_SMTP_ASNH_TEMPLATE_UNRECOGNIZED_TOKEN.get(
469 tokenStr, delimPos, lineNumber));
470 }
471
472 startPos = closeDelimPos + 2;
473 }
474 }
475 }
476
477
478 // We need to put a CRLF at the end of the line, as per the SMTP spec.
479 elementList.add(new TextNotificationMessageTemplateElement("\r\n"));
480 }
481
482 return elementList;
483 }
484 catch (Exception e)
485 {
486 if (debugEnabled())
487 {
488 TRACER.debugCaught(DebugLogLevel.ERROR, e);
489 }
490
491 throw new ConfigException(ERR_SMTP_ASNH_TEMPLATE_CANNOT_PARSE.get(
492 f.getAbsolutePath(),
493 currentConfig.dn().toString(),
494 getExceptionMessage(e)));
495 }
496 finally
497 {
498 try
499 {
500 if (reader != null)
501 {
502 reader.close();
503 }
504 } catch (Exception e) {}
505 }
506 }
507
508
509
510 /**
511 * {@inheritDoc}
512 */
513 public boolean isConfigurationAcceptable(
514 AccountStatusNotificationHandlerCfg
515 configuration,
516 List<Message> unacceptableReasons)
517 {
518 SMTPAccountStatusNotificationHandlerCfg config =
519 (SMTPAccountStatusNotificationHandlerCfg) configuration;
520 return isConfigurationChangeAcceptable(config, unacceptableReasons);
521 }
522
523
524
525 /**
526 * {@inheritDoc}
527 */
528 public void handleStatusNotification(AccountStatusNotification notification)
529 {
530 SMTPAccountStatusNotificationHandlerCfg config = currentConfig;
531 HashMap<AccountStatusNotificationType,String> subjects = subjectMap;
532 HashMap<AccountStatusNotificationType,
533 List<NotificationMessageTemplateElement>> templates = templateMap;
534
535
536 // First, see if the notification type is one that we handle. If not, then
537 // return without doing anything.
538 AccountStatusNotificationType notificationType =
539 notification.getNotificationType();
540 List<NotificationMessageTemplateElement> templateElements =
541 templates.get(notificationType);
542 if (templateElements == null)
543 {
544 if (debugEnabled())
545 {
546 TRACER.debugInfo("No message template for notification type " +
547 notificationType.getName());
548 }
549
550 return;
551 }
552
553
554 // It is a notification that should be handled, so we can start generating
555 // the e-mail message. First, check to see if there are any mail attributes
556 // that would cause us to send a message to the end user.
557 LinkedList<String> recipients = new LinkedList<String>();
558 Set<AttributeType> addressAttrs = config.getEmailAddressAttributeType();
559 Set<String> recipientAddrs = config.getRecipientAddress();
560 if ((addressAttrs != null) && (! addressAttrs.isEmpty()))
561 {
562 Entry userEntry = notification.getUserEntry();
563 for (AttributeType t : addressAttrs)
564 {
565 List<Attribute> attrList = userEntry.getAttribute(t);
566 if (attrList != null)
567 {
568 for (Attribute a : attrList)
569 {
570 for (AttributeValue v : a.getValues())
571 {
572 if (debugEnabled())
573 {
574 TRACER.debugInfo("Adding end user recipient " +
575 v.getStringValue() + " from attr " +
576 a.getNameWithOptions());
577 }
578
579 recipients.add(v.getStringValue());
580 }
581 }
582 }
583 }
584
585 if (recipients.isEmpty())
586 {
587 if ((recipientAddrs == null) || recipientAddrs.isEmpty())
588 {
589 // There are no recipients at all, so there's no point in generating
590 // the message. Return without doing anything.
591 if (debugEnabled())
592 {
593 TRACER.debugInfo("No end user recipients, and no explicit " +
594 "recipients");
595 }
596
597 return;
598 }
599 else
600 {
601 if (! config.isSendMessageWithoutEndUserAddress())
602 {
603 // We can't send the message to the end user, and the handler is
604 // configured to not send only to administrators, so we shouln't
605 // do anything.
606 if (debugEnabled())
607 {
608 TRACER.debugInfo("No end user recipients, and shouldn't send " +
609 "without end user recipients");
610 }
611
612 return;
613 }
614 }
615 }
616 }
617
618
619 // Next, add any explicitly-defined recipients.
620 if (recipientAddrs != null)
621 {
622 if (debugEnabled())
623 {
624 for (String s : recipientAddrs)
625 {
626 TRACER.debugInfo("Adding explicit recipient " + s);
627 }
628 }
629
630 recipients.addAll(recipientAddrs);
631 }
632
633
634 // Get the message subject to use. If none is defined, then use a generic
635 // subject.
636 String subject = subjects.get(notificationType);
637 if (subject == null)
638 {
639 subject = INFO_SMTP_ASNH_DEFAULT_SUBJECT.get().toString();
640
641 if (debugEnabled())
642 {
643 TRACER.debugInfo("Using default subject of " + subject);
644 }
645 }
646 else if (debugEnabled())
647 {
648 TRACER.debugInfo("Using per-type subject of " + subject);
649 }
650
651
652
653 // Generate the message body.
654 MessageBuilder messageBody = new MessageBuilder();
655 for (NotificationMessageTemplateElement e : templateElements)
656 {
657 e.generateValue(messageBody, notification);
658 }
659
660
661 // Create and send the e-mail message.
662 EMailMessage message = new EMailMessage(config.getSenderAddress(),
663 recipients, subject);
664 message.setBody(messageBody);
665 if (debugEnabled())
666 {
667 TRACER.debugInfo("Set message body of " + messageBody.toString());
668 }
669
670
671 try
672 {
673 message.send();
674
675 if (debugEnabled())
676 {
677 TRACER.debugInfo("Successfully sent the message");
678 }
679 }
680 catch (Exception e)
681 {
682 if (debugEnabled())
683 {
684 TRACER.debugCaught(DebugLogLevel.ERROR, e);
685 }
686
687 logError(ERR_SMTP_ASNH_CANNOT_SEND_MESSAGE.get(notificationType.getName(),
688 notification.getUserDN().toString(),
689 getExceptionMessage(e)));
690 }
691 }
692
693
694
695 /**
696 * {@inheritDoc}
697 */
698 public boolean isConfigurationChangeAcceptable(
699 SMTPAccountStatusNotificationHandlerCfg configuration,
700 List<Message> unacceptableReasons)
701 {
702 boolean configAcceptable = true;
703
704
705 // Make sure that the Directory Server is configured with information about
706 // one or more mail servers.
707 List<Properties> propList = DirectoryServer.getMailServerPropertySets();
708 if ((propList == null) || propList.isEmpty())
709 {
710 unacceptableReasons.add(ERR_SMTP_ASNH_NO_MAIL_SERVERS_CONFIGURED.get(
711 configuration.dn().toString()));
712 configAcceptable = false;
713 }
714
715
716 // Make sure that either an explicit recipient list or a set of email
717 // address attributes were provided.
718 Set<AttributeType> mailAttrs = configuration.getEmailAddressAttributeType();
719 Set<String> recipients = configuration.getRecipientAddress();
720 if (((mailAttrs == null) || mailAttrs.isEmpty()) &&
721 ((recipients == null) || recipients.isEmpty()))
722 {
723 unacceptableReasons.add(ERR_SMTP_ASNH_NO_RECIPIENTS.get(
724 configuration.dn().toString()));
725 configAcceptable = false;
726 }
727
728 try
729 {
730 parseSubjects(configuration);
731 }
732 catch (ConfigException ce)
733 {
734 if (debugEnabled())
735 {
736 TRACER.debugCaught(DebugLogLevel.ERROR, ce);
737 }
738
739 unacceptableReasons.add(ce.getMessageObject());
740 configAcceptable = false;
741 }
742
743 try
744 {
745 parseTemplates(configuration);
746 }
747 catch (ConfigException ce)
748 {
749 if (debugEnabled())
750 {
751 TRACER.debugCaught(DebugLogLevel.ERROR, ce);
752 }
753
754 unacceptableReasons.add(ce.getMessageObject());
755 configAcceptable = false;
756 }
757
758 return configAcceptable;
759 }
760
761
762
763 /**
764 * {@inheritDoc}
765 */
766 public ConfigChangeResult applyConfigurationChange(
767 SMTPAccountStatusNotificationHandlerCfg configuration)
768 {
769 try
770 {
771 HashMap<AccountStatusNotificationType,String> subjects =
772 parseSubjects(configuration);
773 HashMap<AccountStatusNotificationType,
774 List<NotificationMessageTemplateElement>> templates =
775 parseTemplates(configuration);
776
777 currentConfig = configuration;
778 subjectMap = subjects;
779 templateMap = templates;
780 return new ConfigChangeResult(ResultCode.SUCCESS, false);
781 }
782 catch (ConfigException ce)
783 {
784 if (debugEnabled())
785 {
786 TRACER.debugCaught(DebugLogLevel.ERROR, ce);
787 }
788
789 LinkedList<Message> messageList = new LinkedList<Message>();
790 messageList.add(ce.getMessageObject());
791
792 return new ConfigChangeResult(ResultCode.UNWILLING_TO_PERFORM, false,
793 messageList);
794 }
795 }
796 }
797