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