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 java.io.BufferedReader;
023import java.io.File;
024import java.io.FileReader;
025import java.util.ArrayList;
026import java.util.Date;
027import java.util.LinkedList;
028import java.util.List;
029import java.util.Properties;
030
031import javax.activation.DataHandler;
032import javax.activation.FileDataSource;
033import javax.mail.MessagingException;
034import javax.mail.SendFailedException;
035import javax.mail.Session;
036import javax.mail.Transport;
037import javax.mail.internet.InternetAddress;
038import javax.mail.internet.MimeBodyPart;
039import javax.mail.internet.MimeMessage;
040import javax.mail.internet.MimeMultipart;
041
042import org.forgerock.i18n.LocalizableMessage;
043import org.forgerock.i18n.LocalizableMessageBuilder;
044import org.opends.server.core.DirectoryServer;
045import org.forgerock.i18n.slf4j.LocalizedLogger;
046
047import com.forgerock.opendj.cli.ArgumentException;
048import com.forgerock.opendj.cli.ArgumentParser;
049import com.forgerock.opendj.cli.BooleanArgument;
050import com.forgerock.opendj.cli.StringArgument;
051
052import static org.opends.messages.ToolMessages.*;
053import static org.opends.messages.UtilityMessages.*;
054import static org.opends.server.util.ServerConstants.*;
055import static org.opends.server.util.StaticUtils.*;
056
057import static com.forgerock.opendj.cli.CommonArguments.*;
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 LinkedList<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
096  /**
097   * Creates a new e-mail message with the provided information.
098   *
099   * @param  sender     The address of the sender for the message.
100   * @param  recipient  The address of the recipient for the message.
101   * @param  subject    The subject to use for the message.
102   */
103  public EMailMessage(String sender, String recipient, String subject)
104  {
105    this.sender  = sender;
106    this.subject = subject;
107
108    recipients = CollectionUtils.newArrayList(recipient);
109
110    body         = new LocalizableMessageBuilder();
111    attachments  = new LinkedList<>();
112    bodyMIMEType = "text/plain";
113  }
114
115
116
117  /**
118   * Creates a new e-mail message with the provided information.
119   *
120   * @param  sender      The address of the sender for the message.
121   * @param  recipients  The addresses of the recipients for the message.
122   * @param  subject     The subject to use for the message.
123   */
124  public EMailMessage(String sender, List<String> recipients,
125                      String subject)
126  {
127    this.sender     = sender;
128    this.recipients = recipients;
129    this.subject    = subject;
130
131    body         = new LocalizableMessageBuilder();
132    attachments  = new LinkedList<>();
133    bodyMIMEType = "text/plain";
134  }
135
136
137
138  /**
139   * Retrieves the sender for this message.
140   *
141   * @return  The sender for this message.
142   */
143  public String getSender()
144  {
145    return sender;
146  }
147
148
149
150  /**
151   * Specifies the sender for this message.
152   *
153   * @param  sender  The sender for this message.
154   */
155  public void setSender(String sender)
156  {
157    this.sender = sender;
158  }
159
160
161
162  /**
163   * Retrieves the set of recipients for this message.  This list may be
164   * directly manipulated by the caller.
165   *
166   * @return  The set of recipients for this message.
167   */
168  public List<String> getRecipients()
169  {
170    return recipients;
171  }
172
173
174
175  /**
176   * Specifies the set of recipients for this message.
177   *
178   * @param  recipients The set of recipients for this message.
179   */
180  public void setRecipients(ArrayList<String> recipients)
181  {
182    this.recipients = recipients;
183  }
184
185
186
187  /**
188   * Adds the specified recipient to this message.
189   *
190   * @param  recipient  The recipient to add to this message.
191   */
192  public void addRecipient(String recipient)
193  {
194    recipients.add(recipient);
195  }
196
197
198
199  /**
200   * Retrieves the subject for this message.
201   *
202   * @return  The subject for this message.
203   */
204  public String getSubject()
205  {
206    return subject;
207  }
208
209
210
211  /**
212   * Specifies the subject for this message.
213   *
214   * @param  subject  The subject for this message.
215   */
216  public void setSubject(String subject)
217  {
218    this.subject = subject;
219  }
220
221
222  /**
223   * Retrieves the MIME Type for the body of this message.
224   *
225   * @return The MIME Type for this message.
226   */
227   public String getBodyMIMEType()
228   {
229     return bodyMIMEType;
230   }
231
232  /**
233   * Specifies the MIME Type for the body of this message.
234   *
235   * @param bodyMIMEType  The MIME Type for this message.
236   */
237  public void setBodyMIMEType(String bodyMIMEType)
238  {
239    this.bodyMIMEType = bodyMIMEType;
240  }
241
242  /**
243   * Retrieves the body for this message.  It may be directly manipulated by the
244   * caller.
245   *
246   * @return  The body for this message.
247   */
248  public LocalizableMessageBuilder getBody()
249  {
250    return body;
251  }
252
253
254
255  /**
256   * Specifies the body for this message.
257   *
258   * @param  body  The body for this message.
259   */
260  public void setBody(LocalizableMessageBuilder body)
261  {
262    this.body = body;
263  }
264
265
266
267  /**
268   * Specifies the body for this message.
269   *
270   * @param  body  The body for this message.
271   */
272  public void setBody(LocalizableMessage body)
273  {
274    this.body = new LocalizableMessageBuilder(body);
275  }
276
277
278
279  /**
280   * Appends the provided text to the body of this message.
281   *
282   * @param  text  The text to append to the body of the message.
283   */
284  public void appendToBody(String text)
285  {
286    body.append(text);
287  }
288
289
290
291  /**
292   * Retrieves the set of attachments for this message.  This list may be
293   * directly modified by the caller if desired.
294   *
295   * @return  The set of attachments for this message.
296   */
297  public LinkedList<MimeBodyPart> getAttachments()
298  {
299    return attachments;
300  }
301
302
303
304  /**
305   * Adds the provided attachment to this mail message.
306   *
307   * @param  attachment  The attachment to add to this mail message.
308   */
309  public void addAttachment(MimeBodyPart attachment)
310  {
311    attachments.add(attachment);
312  }
313
314
315
316  /**
317   * Adds an attachment to this mail message with the provided text.
318   *
319   * @param  attachmentText  The text to include in the attachment.
320   *
321   * @throws  MessagingException  If there is a problem of some type with the
322   *                              attachment.
323   */
324  public void addAttachment(String attachmentText)
325         throws MessagingException
326  {
327    MimeBodyPart attachment = new MimeBodyPart();
328    attachment.setText(attachmentText);
329    attachments.add(attachment);
330  }
331
332
333
334  /**
335   * Adds the provided attachment to this mail message.
336   *
337   * @param  attachmentFile  The file containing the attachment data.
338   *
339   * @throws  MessagingException  If there is a problem of some type with the
340   *                              attachment.
341   */
342  public void addAttachment(File attachmentFile)
343         throws MessagingException
344  {
345    MimeBodyPart attachment = new MimeBodyPart();
346
347    FileDataSource dataSource = new FileDataSource(attachmentFile);
348    attachment.setDataHandler(new DataHandler(dataSource));
349    attachment.setFileName(attachmentFile.getName());
350
351    attachments.add(attachment);
352  }
353
354
355
356  /**
357   * Attempts to send this message to the intended recipient(s).  This will use
358   * the mail server(s) defined in the Directory Server mail handler
359   * configuration.  If multiple servers are specified and the first is
360   * unavailable, then the other server(s) will be tried before returning a
361   * failure to the caller.
362   *
363   * @throws  MessagingException  If a problem occurred while attempting to send
364   *                              the message.
365   */
366  public void send()
367         throws MessagingException
368  {
369    send(DirectoryServer.getMailServerPropertySets());
370  }
371
372
373
374  /**
375   * Attempts to send this message to the intended recipient(s).  If multiple
376   * servers are specified and the first is unavailable, then the other
377   * server(s) will be tried before returning a failure to the caller.
378   *
379   * @param  mailServerPropertySets  A list of property sets providing
380   *                                 information about the mail servers to use
381   *                                 when sending the message.
382   *
383   * @throws  MessagingException  If a problem occurred while attempting to send
384   *                              the message.
385   */
386  public void send(List<Properties> mailServerPropertySets)
387         throws MessagingException
388  {
389    // Get information about the available mail servers that we can use.
390    MessagingException sendException = null;
391    for (Properties props : mailServerPropertySets)
392    {
393      // Get a session and use it to create a new message.
394      Session session = Session.getInstance(props);
395      MimeMessage message = new MimeMessage(session);
396      message.setSubject(subject);
397      message.setSentDate(new Date());
398
399
400      // Add the sender address.  If this fails, then it's a fatal problem we'll
401      // propagate to the caller.
402      try
403      {
404        message.setFrom(new InternetAddress(sender));
405      }
406      catch (MessagingException me)
407      {
408        logger.traceException(me);
409
410        LocalizableMessage msg = ERR_EMAILMSG_INVALID_SENDER_ADDRESS.get(sender, me.getMessage());
411        throw new MessagingException(msg.toString(), me);
412      }
413
414
415      // Add the recipient addresses.  If any of them fail, then that's a fatal
416      // problem we'll propagate to the caller.
417      InternetAddress[] recipientAddresses =
418           new InternetAddress[recipients.size()];
419      for (int i=0; i < recipientAddresses.length; i++)
420      {
421        String recipient = recipients.get(i);
422
423        try
424        {
425          recipientAddresses[i] = new InternetAddress(recipient);
426        }
427        catch (MessagingException me)
428        {
429          logger.traceException(me);
430
431          LocalizableMessage msg = ERR_EMAILMSG_INVALID_RECIPIENT_ADDRESS.get(recipient, me.getMessage());
432          throw new MessagingException(msg.toString(), me);
433        }
434      }
435      message.setRecipients(
436              javax.mail.Message.RecipientType.TO,
437              recipientAddresses);
438
439
440      // If we have any attachments, then the whole thing needs to be
441      // multipart.  Otherwise, just set the text of the message.
442      if (attachments.isEmpty())
443      {
444        message.setContent(body.toString(), bodyMIMEType);
445      }
446      else
447      {
448        MimeMultipart multiPart = new MimeMultipart();
449
450        MimeBodyPart bodyPart = new MimeBodyPart();
451        bodyPart.setText(body.toString());
452        multiPart.addBodyPart(bodyPart);
453
454        for (MimeBodyPart attachment : attachments)
455        {
456          multiPart.addBodyPart(attachment);
457        }
458
459        message.setContent(multiPart);
460      }
461
462
463      // Try to send the message.  If this fails, it can be a complete failure
464      // or a partial one.  If it's a complete failure then try rolling over to
465      // the next server.  If it's a partial one, then that likely means that
466      // the message was sent but one or more recipients was rejected, so we'll
467      // propagate that back to the caller.
468      try
469      {
470        Transport.send(message);
471        return;
472      }
473      catch (SendFailedException sfe)
474      {
475        logger.traceException(sfe);
476
477        // We'll ignore this and hope that another server is available.  If not,
478        // then at least save the exception so that we can throw it if all else
479        // fails.
480        if (sendException == null)
481        {
482          sendException = sfe;
483        }
484      }
485      // FIXME -- Are there any other types of MessagingException that we might
486      //          want to catch so we could try again on another server?
487    }
488
489
490    // If we've gotten here, then we've tried all of the servers in the list and
491    // still failed.  If we captured an earlier exception, then throw it.
492    // Otherwise, throw a generic exception.
493    if (sendException == null)
494    {
495      LocalizableMessage message = ERR_EMAILMSG_CANNOT_SEND.get();
496      throw new MessagingException(message.toString());
497    }
498    else
499    {
500      throw sendException;
501    }
502  }
503
504
505
506  /**
507   * Provide a command-line mechanism for sending an e-mail message via SMTP.
508   *
509   * @param  args  The command-line arguments provided to this program.
510   */
511  public static void main(String[] args)
512  {
513    LocalizableMessage description = INFO_EMAIL_TOOL_DESCRIPTION.get();
514    ArgumentParser argParser = new ArgumentParser(EMailMessage.class.getName(),
515                                                  description, false);
516
517    BooleanArgument showUsage  = null;
518    StringArgument  attachFile = null;
519    StringArgument  bodyFile   = null;
520    StringArgument  host       = null;
521    StringArgument  from       = null;
522    StringArgument  subject    = null;
523    StringArgument  to         = null;
524
525    try
526    {
527      host =
528              StringArgument.builder("host")
529                      .shortIdentifier('h')
530                      .description(INFO_EMAIL_HOST_DESCRIPTION.get())
531                      .multiValued()
532                      .required()
533                      .defaultValue("127.0.0.1")
534                      .valuePlaceholder(INFO_HOST_PLACEHOLDER.get())
535                      .buildAndAddToParser(argParser);
536      from =
537              StringArgument.builder("from")
538                      .shortIdentifier('f')
539                      .description(INFO_EMAIL_FROM_DESCRIPTION.get())
540                      .required()
541                      .valuePlaceholder(INFO_ADDRESS_PLACEHOLDER.get())
542                      .buildAndAddToParser(argParser);
543      to =
544              StringArgument.builder("to")
545                      .shortIdentifier('t')
546                      .description(INFO_EMAIL_TO_DESCRIPTION.get())
547                      .multiValued()
548                      .required()
549                      .valuePlaceholder(INFO_ADDRESS_PLACEHOLDER.get())
550                      .buildAndAddToParser(argParser);
551      subject =
552              StringArgument.builder("subject")
553                      .shortIdentifier('s')
554                      .description(INFO_EMAIL_SUBJECT_DESCRIPTION.get())
555                      .required()
556                      .valuePlaceholder(INFO_SUBJECT_PLACEHOLDER.get())
557                      .buildAndAddToParser(argParser);
558      bodyFile =
559              StringArgument.builder("body")
560                      .shortIdentifier('b')
561                      .description(INFO_EMAIL_BODY_DESCRIPTION.get())
562                      .multiValued()
563                      .required()
564                      .valuePlaceholder(INFO_PATH_PLACEHOLDER.get())
565                      .buildAndAddToParser(argParser);
566      attachFile =
567              StringArgument.builder("attach")
568                      .shortIdentifier('a')
569                      .description(INFO_EMAIL_ATTACH_DESCRIPTION.get())
570                      .multiValued()
571                      .valuePlaceholder(INFO_PATH_PLACEHOLDER.get())
572                      .buildAndAddToParser(argParser);
573
574      showUsage = showUsageArgument();
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