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