001/* 002 * The contents of this file are subject to the terms of the Common Development and 003 * Distribution License (the License). You may not use this file except in compliance with the 004 * License. 005 * 006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the 007 * specific language governing permission and limitations under the License. 008 * 009 * When distributing Covered Software, include this CDDL Header Notice in each file and include 010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL 011 * Header, with the fields enclosed by brackets [] replaced by your own identifying 012 * information: "Portions Copyright [year] [name of copyright owner]". 013 * 014 * Copyright 2006-2008 Sun Microsystems, Inc. 015 * Portions Copyright 2014-2016 ForgeRock AS. 016 * Portions copyright 2015 Edan Idzerda 017 */ 018package org.opends.server.util; 019 020 021 022import static com.forgerock.opendj.cli.CommonArguments.*; 023 024import static org.opends.messages.ToolMessages.*; 025import static org.opends.messages.UtilityMessages.*; 026import static org.opends.server.util.ServerConstants.*; 027import static org.opends.server.util.StaticUtils.*; 028 029import java.io.BufferedReader; 030import java.io.File; 031import java.io.FileReader; 032import java.util.ArrayList; 033import java.util.Date; 034import java.util.LinkedList; 035import java.util.List; 036import java.util.Properties; 037 038import javax.activation.DataHandler; 039import javax.activation.FileDataSource; 040import javax.mail.MessagingException; 041import javax.mail.SendFailedException; 042import javax.mail.Session; 043import javax.mail.Transport; 044import javax.mail.internet.InternetAddress; 045import javax.mail.internet.MimeBodyPart; 046import javax.mail.internet.MimeMessage; 047import javax.mail.internet.MimeMultipart; 048 049import org.forgerock.i18n.LocalizableMessage; 050import org.forgerock.i18n.LocalizableMessageBuilder; 051import org.forgerock.i18n.slf4j.LocalizedLogger; 052import org.opends.server.core.DirectoryServer; 053 054import com.forgerock.opendj.cli.ArgumentException; 055import com.forgerock.opendj.cli.ArgumentParser; 056import com.forgerock.opendj.cli.BooleanArgument; 057import com.forgerock.opendj.cli.StringArgument; 058 059 060 061/** 062 * This class defines an e-mail message that may be sent to one or more 063 * recipients via SMTP. This is a wrapper around JavaMail to make this process 064 * more convenient and fit better into the Directory Server framework. 065 */ 066@org.opends.server.types.PublicAPI( 067 stability=org.opends.server.types.StabilityLevel.VOLATILE, 068 mayInstantiate=true, 069 mayExtend=false, 070 mayInvoke=true) 071public final class EMailMessage 072{ 073 private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); 074 075 076 /** The addresses of the recipients to whom this message should be sent. */ 077 private List<String> recipients; 078 079 /** The set of attachments to include in this message. */ 080 private final List<MimeBodyPart> attachments; 081 082 /** The MIME type for the message body. */ 083 private String bodyMIMEType; 084 085 /** The address of the sender for this message. */ 086 private String sender; 087 088 /** The subject for the mail message. */ 089 private String subject; 090 091 /** The body for the mail message. */ 092 private LocalizableMessageBuilder body; 093 094 /** 095 * Creates a new e-mail message with the provided information. 096 * 097 * @param sender The address of the sender for the message. 098 * @param recipients The addresses of the recipients for the message. 099 * @param subject The subject to use for the message. 100 */ 101 public EMailMessage(String sender, List<String> recipients, 102 String subject) 103 { 104 this.sender = sender; 105 this.recipients = recipients; 106 this.subject = subject; 107 108 body = new LocalizableMessageBuilder(); 109 attachments = new LinkedList<>(); 110 bodyMIMEType = "text/plain"; 111 } 112 113 114 115 /** 116 * Retrieves the sender for this message. 117 * 118 * @return The sender for this message. 119 */ 120 public String getSender() 121 { 122 return sender; 123 } 124 125 126 127 /** 128 * Specifies the sender for this message. 129 * 130 * @param sender The sender for this message. 131 */ 132 public void setSender(String sender) 133 { 134 this.sender = sender; 135 } 136 137 138 139 /** 140 * Retrieves the set of recipients for this message. This list may be 141 * directly manipulated by the caller. 142 * 143 * @return The set of recipients for this message. 144 */ 145 public List<String> getRecipients() 146 { 147 return recipients; 148 } 149 150 151 152 /** 153 * Specifies the set of recipients for this message. 154 * 155 * @param recipients The set of recipients for this message. 156 */ 157 public void setRecipients(ArrayList<String> recipients) 158 { 159 this.recipients = recipients; 160 } 161 162 163 164 /** 165 * Adds the specified recipient to this message. 166 * 167 * @param recipient The recipient to add to this message. 168 */ 169 public void addRecipient(String recipient) 170 { 171 recipients.add(recipient); 172 } 173 174 175 176 /** 177 * Retrieves the subject for this message. 178 * 179 * @return The subject for this message. 180 */ 181 public String getSubject() 182 { 183 return subject; 184 } 185 186 187 188 /** 189 * Specifies the subject for this message. 190 * 191 * @param subject The subject for this message. 192 */ 193 public void setSubject(String subject) 194 { 195 this.subject = subject; 196 } 197 198 199 /** 200 * Retrieves the MIME Type for the body of this message. 201 * 202 * @return The MIME Type for this message. 203 */ 204 public String getBodyMIMEType() 205 { 206 return bodyMIMEType; 207 } 208 209 /** 210 * Specifies the MIME Type for the body of this message. 211 * 212 * @param bodyMIMEType The MIME Type for this message. 213 */ 214 public void setBodyMIMEType(String bodyMIMEType) 215 { 216 this.bodyMIMEType = bodyMIMEType; 217 } 218 219 /** 220 * Retrieves the body for this message. It may be directly manipulated by the 221 * caller. 222 * 223 * @return The body for this message. 224 */ 225 public LocalizableMessageBuilder getBody() 226 { 227 return body; 228 } 229 230 231 232 /** 233 * Specifies the body for this message. 234 * 235 * @param body The body for this message. 236 */ 237 public void setBody(LocalizableMessageBuilder body) 238 { 239 this.body = body; 240 } 241 242 243 244 /** 245 * Specifies the body for this message. 246 * 247 * @param body The body for this message. 248 */ 249 public void setBody(LocalizableMessage body) 250 { 251 this.body = new LocalizableMessageBuilder(body); 252 } 253 254 255 256 /** 257 * Appends the provided text to the body of this message. 258 * 259 * @param text The text to append to the body of the message. 260 */ 261 public void appendToBody(String text) 262 { 263 body.append(text); 264 } 265 266 267 268 /** 269 * Retrieves the set of attachments for this message. This list may be 270 * directly modified by the caller if desired. 271 * 272 * @return The set of attachments for this message. 273 */ 274 public List<MimeBodyPart> getAttachments() 275 { 276 return attachments; 277 } 278 279 280 281 /** 282 * Adds the provided attachment to this mail message. 283 * 284 * @param attachment The attachment to add to this mail message. 285 */ 286 public void addAttachment(MimeBodyPart attachment) 287 { 288 attachments.add(attachment); 289 } 290 291 292 293 /** 294 * Adds an attachment to this mail message with the provided text. 295 * 296 * @param attachmentText The text to include in the attachment. 297 * 298 * @throws MessagingException If there is a problem of some type with the 299 * attachment. 300 */ 301 public void addAttachment(String attachmentText) 302 throws MessagingException 303 { 304 MimeBodyPart attachment = new MimeBodyPart(); 305 attachment.setText(attachmentText); 306 attachments.add(attachment); 307 } 308 309 310 311 /** 312 * Adds the provided attachment to this mail message. 313 * 314 * @param attachmentFile The file containing the attachment data. 315 * 316 * @throws MessagingException If there is a problem of some type with the 317 * attachment. 318 */ 319 private void addAttachment(File attachmentFile) 320 throws MessagingException 321 { 322 MimeBodyPart attachment = new MimeBodyPart(); 323 324 FileDataSource dataSource = new FileDataSource(attachmentFile); 325 attachment.setDataHandler(new DataHandler(dataSource)); 326 attachment.setFileName(attachmentFile.getName()); 327 328 attachments.add(attachment); 329 } 330 331 332 333 /** 334 * Attempts to send this message to the intended recipient(s). This will use 335 * the mail server(s) defined in the Directory Server mail handler 336 * configuration. If multiple servers are specified and the first is 337 * unavailable, then the other server(s) will be tried before returning a 338 * failure to the caller. 339 * 340 * @throws MessagingException If a problem occurred while attempting to send 341 * the message. 342 */ 343 public void send() 344 throws MessagingException 345 { 346 send(DirectoryServer.getMailServerPropertySets()); 347 } 348 349 350 351 /** 352 * Attempts to send this message to the intended recipient(s). If multiple 353 * servers are specified and the first is unavailable, then the other 354 * server(s) will be tried before returning a failure to the caller. 355 * 356 * @param mailServerPropertySets A list of property sets providing 357 * information about the mail servers to use 358 * when sending the message. 359 * 360 * @throws MessagingException If a problem occurred while attempting to send 361 * the message. 362 */ 363 private void send(List<Properties> mailServerPropertySets) 364 throws MessagingException 365 { 366 // Get information about the available mail servers that we can use. 367 MessagingException sendException = null; 368 for (Properties props : mailServerPropertySets) 369 { 370 // Get a session and use it to create a new message. 371 Session session = Session.getInstance(props); 372 MimeMessage message = new MimeMessage(session); 373 message.setSubject(subject); 374 message.setSentDate(new Date()); 375 376 377 // Add the sender address. If this fails, then it's a fatal problem we'll 378 // propagate to the caller. 379 try 380 { 381 message.setFrom(new InternetAddress(sender)); 382 } 383 catch (MessagingException me) 384 { 385 logger.traceException(me); 386 387 LocalizableMessage msg = ERR_EMAILMSG_INVALID_SENDER_ADDRESS.get(sender, me.getMessage()); 388 throw new MessagingException(msg.toString(), me); 389 } 390 391 392 // Add the recipient addresses. If any of them fail, then that's a fatal 393 // problem we'll propagate to the caller. 394 InternetAddress[] recipientAddresses = 395 new InternetAddress[recipients.size()]; 396 for (int i=0; i < recipientAddresses.length; i++) 397 { 398 String recipient = recipients.get(i); 399 400 try 401 { 402 recipientAddresses[i] = new InternetAddress(recipient); 403 } 404 catch (MessagingException me) 405 { 406 logger.traceException(me); 407 408 LocalizableMessage msg = ERR_EMAILMSG_INVALID_RECIPIENT_ADDRESS.get(recipient, me.getMessage()); 409 throw new MessagingException(msg.toString(), me); 410 } 411 } 412 message.setRecipients( 413 javax.mail.Message.RecipientType.TO, 414 recipientAddresses); 415 416 417 // If we have any attachments, then the whole thing needs to be 418 // multipart. Otherwise, just set the text of the message. 419 if (attachments.isEmpty()) 420 { 421 message.setContent(body.toString(), bodyMIMEType); 422 } 423 else 424 { 425 MimeMultipart multiPart = new MimeMultipart(); 426 427 MimeBodyPart bodyPart = new MimeBodyPart(); 428 bodyPart.setText(body.toString()); 429 multiPart.addBodyPart(bodyPart); 430 431 for (MimeBodyPart attachment : attachments) 432 { 433 multiPart.addBodyPart(attachment); 434 } 435 436 message.setContent(multiPart); 437 } 438 439 440 // Try to send the message. If this fails, it can be a complete failure 441 // or a partial one. If it's a complete failure then try rolling over to 442 // the next server. If it's a partial one, then that likely means that 443 // the message was sent but one or more recipients was rejected, so we'll 444 // propagate that back to the caller. 445 try 446 { 447 Transport.send(message); 448 return; 449 } 450 catch (SendFailedException sfe) 451 { 452 logger.traceException(sfe); 453 454 // We'll ignore this and hope that another server is available. If not, 455 // then at least save the exception so that we can throw it if all else 456 // fails. 457 if (sendException == null) 458 { 459 sendException = sfe; 460 } 461 } 462 // FIXME -- Are there any other types of MessagingException that we might 463 // want to catch so we could try again on another server? 464 } 465 466 467 // If we've gotten here, then we've tried all of the servers in the list and 468 // still failed. If we captured an earlier exception, then throw it. 469 // Otherwise, throw a generic exception. 470 if (sendException != null) 471 { 472 throw sendException; 473 } 474 throw new MessagingException(ERR_EMAILMSG_CANNOT_SEND.get().toString()); 475 } 476 477 478 479 /** 480 * Provide a command-line mechanism for sending an e-mail message via SMTP. 481 * 482 * @param args The command-line arguments provided to this program. 483 */ 484 public static void main(String[] args) 485 { 486 LocalizableMessage description = INFO_EMAIL_TOOL_DESCRIPTION.get(); 487 ArgumentParser argParser = new ArgumentParser(EMailMessage.class.getName(), 488 description, false); 489 490 BooleanArgument showUsage = null; 491 StringArgument attachFile = null; 492 StringArgument bodyFile = null; 493 StringArgument host = null; 494 StringArgument from = null; 495 StringArgument subject = null; 496 StringArgument to = null; 497 498 try 499 { 500 host = 501 StringArgument.builder("host") 502 .shortIdentifier('h') 503 .description(INFO_EMAIL_HOST_DESCRIPTION.get()) 504 .multiValued() 505 .required() 506 .defaultValue("127.0.0.1") 507 .valuePlaceholder(INFO_HOST_PLACEHOLDER.get()) 508 .buildAndAddToParser(argParser); 509 from = 510 StringArgument.builder("from") 511 .shortIdentifier('f') 512 .description(INFO_EMAIL_FROM_DESCRIPTION.get()) 513 .required() 514 .valuePlaceholder(INFO_ADDRESS_PLACEHOLDER.get()) 515 .buildAndAddToParser(argParser); 516 to = 517 StringArgument.builder("to") 518 .shortIdentifier('t') 519 .description(INFO_EMAIL_TO_DESCRIPTION.get()) 520 .multiValued() 521 .required() 522 .valuePlaceholder(INFO_ADDRESS_PLACEHOLDER.get()) 523 .buildAndAddToParser(argParser); 524 subject = 525 StringArgument.builder("subject") 526 .shortIdentifier('s') 527 .description(INFO_EMAIL_SUBJECT_DESCRIPTION.get()) 528 .required() 529 .valuePlaceholder(INFO_SUBJECT_PLACEHOLDER.get()) 530 .buildAndAddToParser(argParser); 531 bodyFile = 532 StringArgument.builder("body") 533 .shortIdentifier('b') 534 .description(INFO_EMAIL_BODY_DESCRIPTION.get()) 535 .multiValued() 536 .required() 537 .valuePlaceholder(INFO_PATH_PLACEHOLDER.get()) 538 .buildAndAddToParser(argParser); 539 attachFile = 540 StringArgument.builder("attach") 541 .shortIdentifier('a') 542 .description(INFO_EMAIL_ATTACH_DESCRIPTION.get()) 543 .multiValued() 544 .valuePlaceholder(INFO_PATH_PLACEHOLDER.get()) 545 .buildAndAddToParser(argParser); 546 547 showUsage = showUsageArgument(); 548 argParser.addArgument(showUsage); 549 argParser.setUsageArgument(showUsage); 550 } 551 catch (ArgumentException ae) 552 { 553 System.err.println(ERR_CANNOT_INITIALIZE_ARGS.get(ae.getMessage())); 554 System.exit(1); 555 } 556 557 try 558 { 559 argParser.parseArguments(args); 560 } 561 catch (ArgumentException ae) 562 { 563 argParser.displayMessageAndUsageReference(System.err, ERR_ERROR_PARSING_ARGS.get(ae.getMessage())); 564 System.exit(1); 565 } 566 567 if (showUsage.isPresent()) 568 { 569 return; 570 } 571 572 LinkedList<Properties> mailServerProperties = new LinkedList<>(); 573 for (String s : host.getValues()) 574 { 575 Properties p = new Properties(); 576 p.setProperty(SMTP_PROPERTY_HOST, s); 577 mailServerProperties.add(p); 578 } 579 580 EMailMessage message = new EMailMessage(from.getValue(), to.getValues(), 581 subject.getValue()); 582 583 for (String s : bodyFile.getValues()) 584 { 585 try 586 { 587 File f = new File(s); 588 if (! f.exists()) 589 { 590 System.err.println(ERR_EMAIL_NO_SUCH_BODY_FILE.get(s)); 591 System.exit(1); 592 } 593 594 BufferedReader reader = new BufferedReader(new FileReader(f)); 595 while (true) 596 { 597 String line = reader.readLine(); 598 if (line == null) 599 { 600 break; 601 } 602 603 message.appendToBody(line); 604 message.appendToBody("\r\n"); // SMTP says we should use CRLF. 605 } 606 607 reader.close(); 608 } 609 catch (Exception e) 610 { 611 System.err.println(ERR_EMAIL_CANNOT_PROCESS_BODY_FILE.get(s, 612 getExceptionMessage(e))); 613 System.exit(1); 614 } 615 } 616 617 if (attachFile.isPresent()) 618 { 619 for (String s : attachFile.getValues()) 620 { 621 File f = new File(s); 622 if (! f.exists()) 623 { 624 System.err.println(ERR_EMAIL_NO_SUCH_ATTACHMENT_FILE.get(s)); 625 System.exit(1); 626 } 627 628 try 629 { 630 message.addAttachment(f); 631 } 632 catch (Exception e) 633 { 634 System.err.println(ERR_EMAIL_CANNOT_ATTACH_FILE.get(s, 635 getExceptionMessage(e))); 636 } 637 } 638 } 639 640 try 641 { 642 message.send(mailServerProperties); 643 } 644 catch (Exception e) 645 { 646 System.err.println(ERR_EMAIL_CANNOT_SEND_MESSAGE.get( 647 getExceptionMessage(e))); 648 System.exit(1); 649 } 650 } 651} 652