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