All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.sun.mail.util.logging.MailHandler Maven / Gradle / Ivy

There is a newer version: 1.6.2
Show newest version
/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 2009-2014 Oracle and/or its affiliates. All rights reserved.
 * Copyright (c) 2009-2014 Jason Mehrens. All rights reserved.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License.  You can
 * obtain a copy of the License at
 * https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html
 * or packager/legal/LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 *
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at packager/legal/LICENSE.txt.
 *
 * GPL Classpath Exception:
 * Oracle designates this particular file as subject to the "Classpath"
 * exception as provided by Oracle in the GPL Version 2 section of the License
 * file that accompanied this code.
 *
 * Modifications:
 * If applicable, add the following below the License Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyright [year] [name of copyright owner]"
 *
 * Contributor(s):
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */

package com.sun.mail.util.logging;

import com.sun.mail.smtp.SMTPTransport;
import java.io.*;
import java.lang.reflect.Array;
import java.net.InetAddress;
import java.net.URLConnection;
import java.net.UnknownHostException;
import java.nio.charset.Charset;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.*;
import java.util.logging.*;
import java.util.logging.Formatter;
import javax.activation.DataHandler;
import javax.activation.DataSource;
import javax.activation.FileTypeMap;
import javax.activation.MimetypesFileTypeMap;
import javax.mail.*;
import javax.mail.internet.*;
import javax.mail.util.ByteArrayDataSource;

/**
 * Handler that formats log records as an email message.
 *
 * 

* This Handler will store a fixed number of log records used to * generate a single email message. When the internal buffer reaches capacity, * all log records are formatted and placed in an email which is sent to an * email server. The code to manually setup this handler can be as simple as * the following: * *

 *      Properties props = new Properties();
 *      props.put("mail.smtp.host", "my-mail-server");
 *      props.put("mail.to", "[email protected]");
 *      props.put("verify", "local");
 *      MailHandler h = new MailHandler(props);
 *      h.setLevel(Level.WARNING);
 * 
* *

* Configuration: * The LogManager should define at least one or more recipient addresses and a * mail host for outgoing email. The code to setup this handler via the * logging properties can be as simple as the following: * *

 *      #Default MailHandler settings.
 *      com.sun.mail.util.logging.MailHandler.mail.smtp.host = my-mail-server
 *      com.sun.mail.util.logging.MailHandler.mail.to = [email protected]
 *      com.sun.mail.util.logging.MailHandler.level = WARNING
 *      com.sun.mail.util.logging.MailHandler.verify = local
 * 
* * For a custom handler, e.g. com.foo.MyHandler, the properties would * be: * *
 *      #Subclass com.foo.MyHandler settings.
 *      com.foo.MyHandler.mail.smtp.host = my-mail-server
 *      com.foo.MyHandler.mail.to = [email protected]
 *      com.foo.MyHandler.level = WARNING
 *      com.foo.MyHandler.verify = local
 * 
* * All mail properties documented in the Java Mail API cascade to the * LogManager by prefixing a key using the fully qualified class name of this * MailHandler or the fully qualified derived class name dot mail * property. If the prefixed property is not found, then the mail property * itself is searched in the LogManager. By default each MailHandler is * initialized using the following LogManager configuration properties where * <handler-name> refers to the fully-qualified class name of the * handler. If properties are not defined, or contain invalid values, then the * specified default values are used. * *
    *
  • <handler-name>.attachment.filters a comma * separated list of Filter class names used to create each attachment. * The literal null is reserved for attachments that do not require * filtering. (defaults to the * {@linkplain java.util.logging.Handler#getFilter() body} filter) * *
  • <handler-name>.attachment.formatters a comma * separated list of Formatter class names used to create each * attachment. (default is no attachments) * *
  • <handler-name>.attachment.names a comma separated * list of names or Formatter class names of each attachment. The * attachment file names must not contain any line breaks. * (default is {@linkplain java.util.logging.Formatter#toString() toString} * of the attachment formatter) * *
  • <handler-name>.authenticator name of an * {@linkplain javax.mail.Authenticator} class used to provide login credentials * to the email server or string literal that is the password used with the * {@linkplain Authenticator#getDefaultUserName() default} user name. * (default is null) * *
  • <handler-name>.capacity the max number of * LogRecord objects include in each email message. * (defaults to 1000) * *
  • <handler-name>.comparator name of a * {@linkplain java.util.Comparator} class used to sort the published * LogRecord objects prior to all formatting. * (defaults to null meaning records are unsorted). * *
  • <handler-name>.comparator.reverse a boolean * true to reverse the order of the specified comparator or * false to retain the original order. (defaults to false) * *
  • <handler-name>.encoding the name of the Java * {@linkplain java.nio.charset.Charset#name() character set} to use for the * email message. (defaults to null, the * {@linkplain javax.mail.internet.MimeUtility#getDefaultJavaCharset() default} * platform encoding). * *
  • <handler-name>.errorManager name of an * ErrorManager class used to handle any configuration or mail * transport problems. (defaults to java.util.logging.ErrorManager) * *
  • <handler-name>.filter name of a Filter * class used for the body of the message. (defaults to null, * allow all records) * *
  • <handler-name>.formatter name of a * Formatter class used to format the body of this message. * (defaults to java.util.logging.SimpleFormatter) * *
  • <handler-name>.level specifies the default level * for this Handler (defaults to Level.WARNING). * *
  • <handler-name>.mail.bcc a comma separated list of * addresses which will be blind carbon copied. Typically, this is set to the * recipients that may need to be privately notified of a log message or * notified that a log message was sent to a third party such as a support team. * The empty string can be used to specify no blind carbon copied address. * (defaults to null, none) * *
  • <handler-name>.mail.cc a comma separated list of * addresses which will be carbon copied. Typically, this is set to the * recipients that may need to be notified of a log message but, are not * required to provide direct support. The empty string can be used to specify * no carbon copied address. (defaults to null, none) * *
  • <handler-name>.mail.from a comma separated list of * addresses which will be from addresses. Typically, this is set to the email * address identifying the user running the application. The empty string can * be used to override the default behavior and specify no from address. * (defaults to the {@linkplain javax.mail.Message#setFrom() local address}) * *
  • <handler-name>.mail.host the host name or IP * address of the email server. (defaults to null, use * {@linkplain Transport#protocolConnect default} * Java Mail behavior) * *
  • <handler-name>.mail.reply.to a comma separated * list of addresses which will be reply-to addresses. Typically, this is set * to the recipients that provide support for the application itself. The empty * string can be used to specify no reply-to address. * (defaults to null, none) * *
  • <handler-name>.mail.to a comma separated list of * addresses which will be send-to addresses. Typically, this is set to the * recipients that provide support for the application, system, and/or * supporting infrastructure. The empty string can be used to specify no * send-to address which overrides the default behavior. (defaults to * {@linkplain javax.mail.internet.InternetAddress#getLocalAddress * local address}.) * *
  • <handler-name>.mail.sender a single address * identifying sender of the email; never equal to the from address. Typically, * this is set to the email address identifying the application itself. The * empty string can be used to specify no sender address. * (defaults to null, none) * *
  • <handler-name>.subject the name of a * Formatter class or string literal used to create the subject line. * The empty string can be used to specify no subject. The subject line must * not contain any line breaks. (defaults to the empty string) * *
  • <handler-name>.pushFilter the name of a * Filter class used to trigger an early push. * (defaults to null, no early push) * *
  • <handler-name>.pushLevel the level which will * trigger an early push. (defaults to Level.OFF, only push when full) * *
  • <handler-name>.verify used to * verify the Handler configuration prior to a push. *
      *
    • If the value is not set, equal to an empty string, or equal to the * literal null then no settings are verified prior to a push. *
    • If set to a value of limited then the Handler will * verify minimal local machine settings. *
    • If set to a value of local the Handler will verify * all of settings of the local machine. *
    • If set to a value of resolve, the Handler will * verify all local settings and try to resolve the remote host name with * the domain name server. *
    • If set to a value of remote, the Handler will * verify all local settings and try to establish a connection with the * email server. *
    * If this Handler is only implicitly closed by the * LogManager, then verification should be turned on. * (defaults to null, no verify). *
* *

* Normalization: * The error manager, filters, and formatters when loaded from the LogManager * are converted into canonical form inside the MailHandler. The pool of * interned values is limited to each MailHandler object such that no two * MailHandler objects created by the LogManager will be created sharing * identical error managers, filters, or formatters. If a filter or formatter * should not be interned then it is recommended to retain the identity * equals and identity hashCode methods as the implementation. For a filter or * formatter to be interned the class must implement the * {@linkplain java.lang.Object#equals(java.lang.Object) equals} * and {@linkplain java.lang.Object#hashCode() hashCode} methods. * The recommended code to use for stateless filters and formatters is: *

 * public boolean equals(Object obj) {
 *     return obj == null ? false : obj.getClass() == getClass();
 * }
 *
 * public int hashCode() {
 *     return 31 * getClass().hashCode();
 * }
 * 
* *

* Sorting: * All LogRecord objects are ordered prior to formatting if this * Handler has a non null comparator. Developers might be interested * in sorting the formatted email by thread id, time, and sequence properties * of a LogRecord. Where as system administrators might be interested * in sorting the formatted email by thrown, level, time, and sequence * properties of a LogRecord. If comparator for this handler is * null then the order is unspecified. * *

* Formatting: * The main message body is formatted using the Formatter returned by * getFormatter(). Only records that pass the filter returned by * getFilter() will be included in the message body. The subject * Formatter will see all LogRecord objects that were * published regardless of the current Filter. The MIME type of the * message body can be {@linkplain FileTypeMap#setDefaultFileTypeMap overridden} * by adding a MIME {@linkplain MimetypesFileTypeMap entry} using the simple * class name of the body formatter as the file extension. The MIME type of the * attachments can be overridden by changing the attachment file name extension * or by editing the default MIME entry for a specific file name extension. * *

* Attachments: * This Handler allows multiple attachments per each email. * The attachment order maps directly to the array index order in this * Handler with zero index being the first attachment. The number of * attachment formatters controls the number of attachments per email and * the content type of each attachment. The attachment filters determine if a * LogRecord will be included in an attachment. If an attachment * filter is null then all records are included for that attachment. * Attachments without content will be omitted from email message. The * attachment name formatters create the file name for an attachment. * Custom attachment name formatters can be used to generate an attachment name * based on the contents of the attachment. * *

* Push Level and Push Filter: * The push method, push level, and optional push filter can be used to * conditionally trigger a push at or prior to full capacity. When a push * occurs, the current buffer is formatted into an email and is sent to the * email server. If the push method, push level, or push filter trigger a push * then the outgoing email is flagged as high importance with urgent priority. * *

* Buffering: * Log records that are published are stored in an internal buffer. When this * buffer reaches capacity the existing records are formatted and sent in an * email. Any published records can be sent before reaching capacity by * explictly calling the flush, push, or close * methods. If a circular buffer is required then this handler can be wrapped * with a {@linkplain java.util.logging.MemoryHandler} typically with an * equivalent capacity, level, and push level. * *

* Error Handling: * If the transport of an email message fails, the email is converted to * a {@linkplain javax.mail.internet.MimeMessage#writeTo raw} * {@linkplain java.io.ByteArrayOutputStream#toString(java.lang.String) string} * and is then passed as the msg parameter to * {@linkplain Handler#reportError reportError} along with the exception * describing the cause of the failure. This allows custom error managers to * store, {@linkplain javax.mail.internet.MimeMessage#MimeMessage( * javax.mail.Session, java.io.InputStream) reconstruct}, and resend the * original MimeMessage. The message parameter string is not a raw email * if it starts with value returned from Level.SEVERE.getName(). * Custom error managers can use the following test to determine if the * msg parameter from this handler is a raw email: * *

 * public void error(String msg, Exception ex, int code) {
 *      if (msg == null || msg.length() == 0 || msg.startsWith(Level.SEVERE.getName())) {
 *          super.error(msg, ex, code);
 *      } else {
 *          //The 'msg' parameter is a raw email.
 *      }
 * }
 * 
* * @author Jason Mehrens * @since JavaMail 1.4.3 */ public class MailHandler extends Handler { /** * Use the emptyFilterArray method. */ private static final Filter[] EMPTY_FILTERS = new Filter[0]; /** * Use the emptyFormatterArray method. */ private static final Formatter[] EMPTY_FORMATTERS = new Formatter[0]; /** * Min byte size for header data. Used for initial arrays sizing. */ private static final int MIN_HEADER_SIZE = 1024; /** * Cache the off value. */ private static final int offValue = Level.OFF.intValue(); /** * The action to get and set the context class loader. * Load this before it is loaded in the close method. */ private static final GetAndSetContext GET_AND_SET_CCL = new GetAndSetContext(MailHandler.class); /** * A thread local mutex used to prevent logging loops. * The MUTEX has 3 states: * 1. MUTEX_RESET which is the null state. * 2. MUTEX_PUBLISH on first entry of a push or publish. * 3. MUTEX_REPORT when cycle of records is detected. */ private static final ThreadLocal MUTEX = new ThreadLocal(); /** * The marker object used to report a publishing state. */ private static final Level MUTEX_PUBLISH = Level.ALL; /** * The marker object used to report a error reporting state. */ private static final Level MUTEX_REPORT = Level.OFF; /** * Used to turn off security checks. */ private volatile boolean sealed; /** * Determines if we are inside of a push. * Makes the handler properties read-only during a push. */ private boolean isWriting; /** * Holds all of the email server properties. */ private Properties mailProps; /** * Holds the authenticator required to login to the email server. */ private Authenticator auth; /** * Holds the session object used to generate emails. * Sessions can be shared by multiple threads. * See BUGID 6228391 */ private Session session; /** * Holds all of the log records that will be used to create the email. */ private LogRecord[] data; /** * The number of log records in the buffer. */ private int size; /** * The maximum number of log records to format per email. * Used to roughly bound the size of an email. * Every time the capacity is reached, the handler will push. * The capacity will be negative if this handler is closed. * Negative values are used to ensure all records are pushed. */ private int capacity; /** * Used to order all log records prior to formatting. The main email body * and all attachments use the order determined by this comparator. If no * comparator is present the log records will be in no specified order. */ private Comparator comparator; /** * Holds the formatter used to create the subject line of the email. * A subject formatter is not required for the email message. * All published records pass through the subject formatter. */ private Formatter subjectFormatter; /** * Holds the push level for this handler. * This is only required if an email must be sent prior to shutdown * or before the buffer is full. */ private Level pushLevel; /** * Holds the push filter for trigger conditions requiring an early push. * Only gets called if the given log record is greater than or equal * to the push level and the push level is not Level.OFF. */ private Filter pushFilter; /** * Holds the filters for each attachment. Filters are optional for * each attachment. This is declared volatile because this is treated as * copy-on-write. The VO_VOLATILE_REFERENCE_TO_ARRAY warning is a false * positive. */ @SuppressWarnings("VolatileArrayField") private volatile Filter[] attachmentFilters; /** * Holds the formatters that create the content for each attachment. * Each formatter maps directly to an attachment. The formatters * getHead, format, and getTail methods are only called if one or more * log records pass through the attachment filters. */ private Formatter[] attachmentFormatters; /** * Holds the formatters that create the file name for each attachment. * Each formatter must produce a non null and non empty name. * The final file name will be the concatenation of one getHead call, plus * all of the format calls, plus one getTail call. */ private Formatter[] attachmentNames; /** * Used to override the content type for the body and set the content type * for each attachment. */ private FileTypeMap contentTypes; /** * Creates a MailHandler that is configured by the * LogManager configuration properties. * @throws SecurityException if a security manager exists and the * caller does not have LoggingPermission("control"). */ public MailHandler() { init((Properties) null); sealed = true; } /** * Creates a MailHandler that is configured by the * LogManager configuration properties but overrides the * LogManager capacity with the given capacity. * @param capacity of the internal buffer. * @throws IllegalArgumentException if capacity less than one. * @throws SecurityException if a security manager exists and the * caller does not have LoggingPermission("control"). */ public MailHandler(final int capacity) { init((Properties) null); sealed = true; setCapacity0(capacity); } /** * Creates a mail handler with the given mail properties. * The key/value pairs are defined in the Java Mail API * documentation. This Handler will also search the * LogManager for defaults if needed. * @param props a non null properties object. * @throws NullPointerException if props is null. * @throws SecurityException if a security manager exists and the * caller does not have LoggingPermission("control"). */ public MailHandler(final Properties props) { if (props == null) { throw new NullPointerException(); } init(props); sealed = true; setMailProperties0(props); } /** * Check if this Handler would actually log a given * LogRecord into its internal buffer. *

* This method checks if the LogRecord has an appropriate level and * whether it satisfies any Filter including any attachment filters. * However it does not check whether the LogRecord would * result in a "push" of the buffer contents. *

* @param record a LogRecord * @return true if the LogRecord would be logged. */ @Override public boolean isLoggable(final LogRecord record) { int levelValue = getLevel().intValue(); if (record.getLevel().intValue() < levelValue || levelValue == offValue) { return false; } Filter body = getFilter(); if (body == null || body.isLoggable(record)) { return true; } return isAttachmentLoggable(record); } /** * Stores a LogRecord in the internal buffer. *

* The isLoggable method is called to check if the given log record * is loggable. If the given record is loggable, it is copied into * an internal buffer. Then the record's level property is compared with * the push level. If the given level of the LogRecord * is greater than or equal to the push level then the push filter is * called. If no push filter exists, the push filter returns true, * or the capacity of the internal buffer has been reached then all buffered * records are formatted into one email and sent to the server. * * @param record description of the log event. */ public void publish(final LogRecord record) { /** * It is possible for the handler to be closed after the * call to isLoggable. In that case, the current thread * will push to ensure that all published records are sent. * See close(). */ if (tryMutex()) { try { if (isLoggable(record)) { record.getSourceMethodName(); //Infer caller. publish0(record); } } finally { releaseMutex(); } } else { reportUnPublishedError(record); } } /** * Performs the publish after the record has been filtered. * @param record the record. * @since JavaMail 1.4.5 */ private void publish0(final LogRecord record) { Message msg; boolean priority; synchronized (this) { if (size == data.length && size < capacity) { grow(); } if (size < data.length) { data[size] = record; ++size; //Be nice to client compiler. priority = isPushable(record); if (priority || size >= capacity) { msg = writeLogRecords(ErrorManager.WRITE_FAILURE); } else { msg = null; } } else { priority = false; msg = null; } } if (msg != null) { send(msg, priority, ErrorManager.WRITE_FAILURE); } } /** * Report to the error manager that a logging loop was detected and * we are going to break the cycle of messages. It is possible that * a custom error manager could continue the cycle in which case * we will stop trying to report errors. * @param record the record or null. * @since JavaMail 1.4.6 */ private void reportUnPublishedError(LogRecord record) { if (MUTEX_PUBLISH.equals(MUTEX.get())) { MUTEX.set(MUTEX_REPORT); try { final String msg; if (record != null) { final SimpleFormatter f = new SimpleFormatter(); msg = "Log record " + record.getSequenceNumber() + " was not published. " + head(f) + format(f, record) + tail(f, ""); } else { msg = null; } Exception e = new IllegalStateException( "Recursive publish detected by thread " + Thread.currentThread()); reportError(msg, e, ErrorManager.WRITE_FAILURE); } finally { MUTEX.set(MUTEX_PUBLISH); } } } /** * Used to detect reentrance by the current thread to the publish method. * This mutex is thread local scope and will not block other threads. * The state is advanced on if the current thread is in a reset state. * @return true if the mutex was acquired. * @since JavaMail 1.4.6 */ private boolean tryMutex() { if (MUTEX.get() == null) { MUTEX.set(MUTEX_PUBLISH); return true; } else { return false; } } /** * Releases the mutex held by the current thread. * This mutex is thread local scope and will not block other threads. * @since JavaMail 1.4.6 */ private void releaseMutex() { MUTEX.remove(); } /** * Pushes any buffered records to the email server as high importance with * urgent priority. The internal buffer is then cleared. Does nothing if * called from inside a push. * @see #flush() */ public void push() { push(true, ErrorManager.FLUSH_FAILURE); } /** * Pushes any buffered records to the email server as normal priority. * The internal buffer is then cleared. Does nothing if called from inside * a push. * @see #push() */ public void flush() { push(false, ErrorManager.FLUSH_FAILURE); } /** * Prevents any other records from being published. * Pushes any buffered records to the email server as normal priority. * The internal buffer is then cleared. Once this handler is closed it * will remain closed. *

* If this Handler is only implicitly closed by the * LogManager, then verification should be * turned on. * @throws SecurityException if a security manager exists and the * caller does not have LoggingPermission("control"). * @see #flush() */ public void close() { checkAccess(); //Security check first. //The LogManager$Cleaner has a context class loader set to null. //Set the CCL to this class loader for loading content handlers. final Object ccl = getAndSetContextClassLoader(); try { Message msg = null; synchronized (this) { try { msg = writeLogRecords(ErrorManager.CLOSE_FAILURE); } finally { super.setLevel(Level.OFF); //Change level after formatting. /** * The sign bit of the capacity is set to ensure that records * that have passed isLoggable, but have yet to be added to * the internal buffer, are immediately pushed as an email. */ if (this.capacity > 0) { this.capacity = -this.capacity; } //Ensure not inside a push. if (size == 0 && data.length != 1) { this.data = new LogRecord[1]; } } } if (msg != null) { send(msg, false, ErrorManager.CLOSE_FAILURE); } } finally { setContextClassLoader(ccl); } } /** * Set the log level specifying which message levels will be * logged by this Handler. Message levels lower than this * value will be discarded. * @param newLevel the new value for the log level * @throws NullPointerException if newLevel is null. * @throws SecurityException if a security manager exists and * the caller does not have LoggingPermission("control"). */ @Override public synchronized void setLevel(final Level newLevel) { if (this.capacity > 0) { super.setLevel(newLevel); } else { //Don't allow a closed handler to be opened (half way). if (newLevel == null) { throw new NullPointerException(); } checkAccess(); } } /** * Gets the push level. The default is Level.OFF meaning that * this Handler will only push when the internal buffer is full. * @return the push level. */ public final synchronized Level getPushLevel() { return this.pushLevel; } /** * Sets the push level. This level is used to trigger a push so that * all pending records are formatted and sent to the email server. When * the push level triggers a send, the resulting email is flagged as * high importance with urgent priority. * @param level Level object. * @throws NullPointerException if level is null. * @throws SecurityException if a security manager exists and the * caller does not have LoggingPermission("control"). * @throws IllegalStateException if called from inside a push. */ public final synchronized void setPushLevel(final Level level) { checkAccess(); if (level == null) { throw new NullPointerException(); } if (isWriting) { throw new IllegalStateException(); } this.pushLevel = level; } /** * Gets the push filter. The default is null. * @return the push filter or null. */ public final synchronized Filter getPushFilter() { return this.pushFilter; } /** * Sets the push filter. This filter is only called if the given * LogRecord level was greater than the push level. If this * filter returns true, all pending records are formatted and sent * to the email server. When the push filter triggers a send, the resulting * email is flagged as high importance with urgent priority. * @param filter push filter or null * @throws SecurityException if a security manager exists and the * caller does not have LoggingPermission("control"). * @throws IllegalStateException if called from inside a push. */ public final synchronized void setPushFilter(final Filter filter) { checkAccess(); if (isWriting) { throw new IllegalStateException(); } this.pushFilter = filter; } /** * Gets the comparator used to order all LogRecord objects prior * to formatting. If null then the order is unspecified. * @return the LogRecord comparator. */ public final synchronized Comparator getComparator() { return this.comparator; } /** * Sets the comparator used to order all LogRecord objects prior * to formatting. If null then the order is unspecified. * @param c the LogRecord comparator. * @throws SecurityException if a security manager exists and the * caller does not have LoggingPermission("control"). * @throws IllegalStateException if called from inside a push. */ public final synchronized void setComparator(Comparator c) { checkAccess(); if (isWriting) { throw new IllegalStateException(); } this.comparator = c; } /** * Gets the number of log records the internal buffer can hold. When * capacity is reached, Handler will format all LogRecord * objects into one email message. * @return the capacity. */ public final synchronized int getCapacity() { assert capacity != Integer.MIN_VALUE && capacity != 0 : capacity; return Math.abs(capacity); } /** * Gets the Authenticator used to login to the email server. * @return an Authenticator or null if none is required. * @throws SecurityException if a security manager exists and the * caller does not have LoggingPermission("control"). */ public final synchronized Authenticator getAuthenticator() { checkAccess(); return this.auth; } /** * Sets the Authenticator used to login to the email server. * @param auth an Authenticator object or null if none is required. * @throws SecurityException if a security manager exists and the * caller does not have LoggingPermission("control"). * @throws IllegalStateException if called from inside a push. */ public final void setAuthenticator(final Authenticator auth) { this.setAuthenticator0(auth); } /** * Sets the Authenticator used to login to the email server. * @param password a password or null if none is required. * @throws SecurityException if a security manager exists and the * caller does not have LoggingPermission("control"). * @throws IllegalStateException if called from inside a push. * @see String#toCharArray() * @since JavaMail 1.4.6 */ public final void setAuthenticator(final char... password) { if (password == null) { setAuthenticator0((Authenticator) null); } else { setAuthenticator0(new DefaultAuthenticator(new String(password))); } } /** * A private hook to handle possible future overrides. See public method. * @param auth see public method. */ private void setAuthenticator0(final Authenticator auth) { checkAccess(); Session settings; synchronized (this) { if (isWriting) { throw new IllegalStateException(); } this.auth = auth; settings = fixUpSession(); } verifySettings(settings); } /** * Sets the mail properties used for the session. The key/value pairs * are defined in the Java Mail API documentation. This * Handler will also search the LogManager for defaults * if needed. * @param props a non null properties object. * @throws SecurityException if a security manager exists and the * caller does not have LoggingPermission("control"). * @throws NullPointerException if props is null. * @throws IllegalStateException if called from inside a push. */ public final void setMailProperties(Properties props) { this.setMailProperties0(props); } /** * A private hook to handle overrides when the public method is declared * non final. See public method for details. * @param props see public method. */ private void setMailProperties0(Properties props) { checkAccess(); props = (Properties) props.clone(); //Allow subclass. Session settings; synchronized (this) { if (isWriting) { throw new IllegalStateException(); } this.mailProps = props; settings = fixUpSession(); } verifySettings(settings); } /** * Gets a copy of the mail properties used for the session. * @return a non null properties object. * @throws SecurityException if a security manager exists and the * caller does not have LoggingPermission("control"). */ public final Properties getMailProperties() { checkAccess(); final Properties props; synchronized (this) { props = this.mailProps; } return (Properties) props.clone(); } /** * Gets the attachment filters. If the attachment filter does not * allow any LogRecord to be formatted, the attachment may * be omitted from the email. * @return a non null array of attachment filters. */ public final Filter[] getAttachmentFilters() { return readOnlyAttachmentFilters().clone(); } /** * Sets the attachment filters. * @param filters a non null array of filters. A null * index value is allowed. A null value means that all * records are allowed for the attachment at that index. * @throws SecurityException if a security manager exists and the * caller does not have LoggingPermission("control"). * @throws NullPointerException if filters is null * @throws IndexOutOfBoundsException if the number of attachment * name formatters do not match the number of attachment formatters. * @throws IllegalStateException if called from inside a push. */ public final void setAttachmentFilters(Filter... filters) { checkAccess(); filters = copyOf(filters, filters.length, Filter[].class); synchronized (this) { if (this.attachmentFormatters.length != filters.length) { throw attachmentMismatch(this.attachmentFormatters.length, filters.length); } if (isWriting) { throw new IllegalStateException(); } this.attachmentFilters = filters; } } /** * Gets the attachment formatters. This Handler is using * attachments only if the returned array length is non zero. * @return a non null array of formatters. */ public final Formatter[] getAttachmentFormatters() { Formatter[] formatters; synchronized (this) { formatters = this.attachmentFormatters; } return formatters.clone(); } /** * Sets the attachment Formatter object for this handler. * The number of formatters determines the number of attachments per * email. This method should be the first attachment method called. * To remove all attachments, call this method with empty array. * @param formatters a non null array of formatters. * @throws SecurityException if a security manager exists and the * caller does not have LoggingPermission("control"). * @throws NullPointerException if the given array or any array index is * null. * @throws IllegalStateException if called from inside a push. */ public final void setAttachmentFormatters(Formatter... formatters) { checkAccess(); if (formatters.length == 0) { //Null check and length check. formatters = emptyFormatterArray(); } else { formatters = copyOf(formatters, formatters.length, Formatter[].class); for (int i = 0; i < formatters.length; ++i) { if (formatters[i] == null) { throw new NullPointerException(atIndexMsg(i)); } } } synchronized (this) { if (isWriting) { throw new IllegalStateException(); } this.attachmentFormatters = formatters; this.fixUpAttachmentFilters(); this.fixUpAttachmentNames(); } } /** * Gets the attachment name formatters. * If the attachment names were set using explicit names then * the names can be returned by calling toString on each * attachment name formatter. * @return non null array of attachment name formatters. */ public final Formatter[] getAttachmentNames() { final Formatter[] formatters; synchronized (this) { formatters = this.attachmentNames; } return formatters.clone(); } /** * Sets the attachment file name for each attachment. The caller must * ensure that the attachment file names do not contain any line breaks. * This method will create a set of custom formatters. * @param names an array of names. * @throws SecurityException if a security manager exists and the * caller does not have LoggingPermission("control"). * @throws IndexOutOfBoundsException if the number of attachment * names do not match the number of attachment formatters. * @throws IllegalArgumentException if any name is empty. * @throws NullPointerException if any given array or name is null. * @throws IllegalStateException if called from inside a push. * @see Character#isISOControl(char) * @see Character#isISOControl(int) */ public final void setAttachmentNames(final String... names) { checkAccess(); final Formatter[] formatters; if (names.length == 0) { formatters = emptyFormatterArray(); } else { formatters = new Formatter[names.length]; } for (int i = 0; i < names.length; ++i) { final String name = names[i]; if (name != null) { if (name.length() > 0) { formatters[i] = new TailNameFormatter(name); } else { throw new IllegalArgumentException(atIndexMsg(i)); } } else { throw new NullPointerException(atIndexMsg(i)); } } synchronized (this) { if (this.attachmentFormatters.length != names.length) { throw attachmentMismatch(this.attachmentFormatters.length, names.length); } if (isWriting) { throw new IllegalStateException(); } this.attachmentNames = formatters; } } /** * Sets the attachment file name formatters. The format method of each * attachment formatter will see only the LogRecord objects that * passed its attachment filter during formatting. The format method will * typically return an empty string. Instead of being used to format * records, it is used to gather information about the contents of an * attachment. The getTail method should be used to construct the * attachment file name and reset any formatter collected state. The * formatter must ensure that the attachment file name does not contain any * line breaks. The toString method of the given formatter should * be overridden to provide a useful attachment file name, if possible. * @param formatters and array of attachment name formatters. * @throws SecurityException if a security manager exists and the * caller does not have LoggingPermission("control"). * @throws IndexOutOfBoundsException if the number of attachment * name formatters do not match the number of attachment formatters. * @throws NullPointerException if any given array or name is null. * @throws IllegalStateException if called from inside a push. * @see Character#isISOControl(char) * @see Character#isISOControl(int) */ public final void setAttachmentNames(Formatter... formatters) { checkAccess(); formatters = copyOf(formatters, formatters.length, Formatter[].class); for (int i = 0; i < formatters.length; ++i) { if (formatters[i] == null) { throw new NullPointerException(atIndexMsg(i)); } } synchronized (this) { if (this.attachmentFormatters.length != formatters.length) { throw attachmentMismatch(this.attachmentFormatters.length, formatters.length); } if (isWriting) { throw new IllegalStateException(); } this.attachmentNames = formatters; } } /** * Gets the formatter used to create the subject line. * If the subject was created using a literal string then * the toString method can be used to get the subject line. * @return the formatter. */ public final synchronized Formatter getSubject() { return this.subjectFormatter; } /** * Sets a literal string for the email subject. The caller must ensure that * the subject line does not contain any line breaks. * @param subject a non null string. * @throws SecurityException if a security manager exists and the * caller does not have LoggingPermission("control"). * @throws NullPointerException if subject is null. * @throws IllegalStateException if called from inside a push. * @see Character#isISOControl(char) * @see Character#isISOControl(int) */ public final void setSubject(final String subject) { if (subject != null) { this.setSubject(new TailNameFormatter(subject)); } else { checkAccess(); throw new NullPointerException(); } } /** * Sets the subject formatter for email. The format method of the subject * formatter will see all LogRecord objects that were published to * this Handler during formatting and will typically return an * empty string. This formatter is used to gather information to create a * summary about what information is contained in the email. The * getTail method should be used to construct the subject and reset * any formatter collected state. The formatter must ensure that the * subject line does not contain any line breaks. The toString * method of the given formatter should be overridden to provide a useful * subject, if possible. * @param format the subject formatter. * @throws SecurityException if a security manager exists and the * caller does not have LoggingPermission("control"). * @throws NullPointerException if format is null. * @throws IllegalStateException if called from inside a push. * @see Character#isISOControl(char) * @see Character#isISOControl(int) */ public final void setSubject(final Formatter format) { checkAccess(); if (format == null) { throw new NullPointerException(); } synchronized (this) { if (isWriting) { throw new IllegalStateException(); } this.subjectFormatter = format; } } /** * Protected convenience method to report an error to this Handler's * ErrorManager. This method will prefix all non null error messages with * Level.SEVERE.getName(). This allows the receiving error * manager to determine if the msg parameter is a simple error * message or a raw email message. * @param msg a descriptive string (may be null) * @param ex an exception (may be null) * @param code an error code defined in ErrorManager */ @Override protected void reportError(String msg, Exception ex, int code) { if (msg != null) { super.reportError(Level.SEVERE.getName() + ": " + msg, ex, code); } else { super.reportError(null, ex, code); } } /** * Calls log manager checkAccess if this is sealed. */ final void checkAccess() { if (sealed) { LogManagerProperties.getLogManager().checkAccess(); } } /** * Determines the mimeType of a formatter from the getHead call. * This could be made protected, or a new class could be created to do * this type of conversion. Currently, this is only used for the body * since the attachments are computed by filename. * Package-private for unit testing. * @param head any head string. * @return return the mime type or null for text/plain. */ final String contentTypeOf(String head) { if (!isEmpty(head)) { final int MAX_CHARS = 25; if (head.length() > MAX_CHARS) { head = head.substring(0, MAX_CHARS); } try { final String encoding = getEncodingName(); final ByteArrayInputStream in = new ByteArrayInputStream(head.getBytes(encoding)); assert in.markSupported() : in.getClass().getName(); return URLConnection.guessContentTypeFromStream(in); } catch (final IOException IOE) { reportError(IOE.getMessage(), IOE, ErrorManager.FORMAT_FAILURE); } } return null; //text/plain } /** * Determines if the given throwable is a no content exception. * Package-private for unit testing. * @param msg the message without content. * @param t the throwable to test. * @return true if the throwable is a missing content exception. * @throws NullPointerException if any of the arguments are null. * @since JavaMail 1.4.5 */ final boolean isMissingContent(Message msg, Throwable t) { for (Throwable cause = t.getCause(); cause != null;) { t = cause; cause = cause.getCause(); } try { msg.writeTo(new ByteArrayOutputStream(MIN_HEADER_SIZE)); } catch (final RuntimeException RE) { throw RE; //Avoid catch all. } catch (final Exception noContent) { final String txt = noContent.getMessage(); if (!isEmpty(txt) && noContent.getClass() == t.getClass()) { return txt.equals(t.getMessage()); } } return false; } /** * Converts a mime message to a raw string or formats the reason * why message can't be changed to raw string and reports it. * @param msg the mime message. * @param ex the original exception. * @param code the ErrorManager code. * @since JavaMail 1.4.5 */ private void reportError(Message msg, Exception ex, int code) { try { //Use super call so we do not prefix raw email. super.reportError(toRawString(msg), ex, code); } catch (final MessagingException rawMe) { reportError(toMsgString(rawMe), ex, code); } catch (final IOException rawIo) { reportError(toMsgString(rawIo), ex, code); } } /** * Determines the mimeType from the given file name. * Used to override the body content type and used for all attachments. * @param name the file name or class name. * @return the mime type or null for text/plain. */ private String getContentType(final String name) { assert Thread.holdsLock(this); final String type = contentTypes.getContentType(name); if ("application/octet-stream".equalsIgnoreCase(type)) { return null; //Formatters return strings, default to text/plain. } return type; } /** * Gets the encoding set for this handler, mime encoding, or file encoding. * @return the java charset name, never null. * @since JavaMail 1.4.5 */ private String getEncodingName() { String encoding = getEncoding(); if (encoding == null) { encoding = MimeUtility.getDefaultJavaCharset(); } return encoding; } /** * Set the content for a part using the encoding assigned to the handler. * @param part the part to assign. * @param buf the formatted data. * @param type the mime type. * @throws MessagingException if there is a problem. */ private void setContent(MimeBodyPart part, CharSequence buf, String type) throws MessagingException { final String encoding = getEncodingName(); if (type != null && !"text/plain".equalsIgnoreCase(type)) { type = contentWithEncoding(type, encoding); try { DataSource source = new ByteArrayDataSource(buf.toString(), type); part.setDataHandler(new DataHandler(source)); } catch (final IOException IOE) { reportError(IOE.getMessage(), IOE, ErrorManager.FORMAT_FAILURE); part.setText(buf.toString(), encoding); } } else { part.setText(buf.toString(), MimeUtility.mimeCharset(encoding)); } } /** * Replaces the charset parameter with the current encoding. * @param type the content type. * @param encoding the java charset name. * @return the type with a specified encoding. */ private String contentWithEncoding(String type, String encoding) { assert encoding != null; try { final ContentType ct = new ContentType(type); ct.setParameter("charset", MimeUtility.mimeCharset(encoding)); encoding = ct.toString(); //See javax.mail.internet.ContentType. if (!isEmpty(encoding)) { type = encoding; } } catch (final MessagingException ME) { reportError(type, ME, ErrorManager.FORMAT_FAILURE); } return type; } /** * Sets the capacity for this handler. This method is kept private * because we would have to define a public policy for when the size is * greater than the capacity. * I.E. do nothing, flush now, truncate now, push now and resize. * @param newCapacity the max number of records. * @throws IllegalStateException if called from inside a push. */ private synchronized void setCapacity0(final int newCapacity) { if (newCapacity <= 0) { throw new IllegalArgumentException("Capacity must be greater than zero."); } if (isWriting) { throw new IllegalStateException(); } if (this.capacity < 0) { //If closed, remain closed. this.capacity = -newCapacity; } else { this.capacity = newCapacity; } } /** * Gets the attachment filters using a happens-before relationship between * this method and setAttachmentFilters. The attachment filters are treated * as copy-on-write, so the returned array must never be modified or * published outside this class. * @return a read only array of filters. */ private Filter[] readOnlyAttachmentFilters() { return this.attachmentFilters; } /** * Factory for empty formatter arrays. * @return an empty array. */ private static Formatter[] emptyFormatterArray() { return EMPTY_FORMATTERS; } /** * Factory for empty filter arrays. * @return an empty array. */ private static Filter[] emptyFilterArray() { return EMPTY_FILTERS; } /** * Expand or shrink the attachment name formatters. * @return true if fixed. */ private boolean fixUpAttachmentNames() { assert Thread.holdsLock(this); boolean fixed = false; final int expect = this.attachmentFormatters.length; final int current = this.attachmentNames.length; if (current != expect) { this.attachmentNames = copyOf(attachmentNames, expect); fixed = current != 0; } //Copy of zero length array is cheap, warm up copyOf. if (expect == 0) { this.attachmentNames = emptyFormatterArray(); assert this.attachmentNames.length == 0; } else { for (int i = 0; i < expect; ++i) { if (this.attachmentNames[i] == null) { this.attachmentNames[i] = new TailNameFormatter( toString(this.attachmentFormatters[i])); } } } return fixed; } /** * Expand or shrink the attachment filters. * @return true if fixed. */ private boolean fixUpAttachmentFilters() { assert Thread.holdsLock(this); boolean fixed = false; final int expect = this.attachmentFormatters.length; final int current = this.attachmentFilters.length; if (current != expect) { this.attachmentFilters = copyOf(attachmentFilters, expect); fixed = current != 0; //Array elements default to null so skip filling if body filter //is null. If not null then only assign to expanded elements. final Filter body = super.getFilter(); if (body != null) { for (int i = current; i < expect; ++i) { this.attachmentFilters[i] = body; } } } //Copy of zero length array is cheap, warm up copyOf. if (expect == 0) { this.attachmentFilters = emptyFilterArray(); assert this.attachmentFilters.length == 0; } return fixed; } /** * Copies the given array. Can be removed when Java Mail requires Java 1.6. * @param the class of the objects in the array. * @param a the original array. * @param len the new size. * @return new copy */ @SuppressWarnings("unchecked") private static T[] copyOf(final T[] a, final int len) { return (T[]) copyOf(a, len, a.getClass()); } /** * Copies the given array to a new array type. * Can be removed when Java Mail requires Java 1.6. * @param the class of the objects in the original array * @param the class of the objects in the returned array * @param a the original array. * @param len the new size. * @param type the array type. * @return new copy */ @SuppressWarnings("unchecked") private static T[] copyOf(U[] a, int len, Class type) { final T[] copy = (T[]) Array.newInstance(type.getComponentType(), len); System.arraycopy(a, 0, copy, 0, Math.min(len, a.length)); return copy; } /** * Sets the size to zero and clears the current buffer. */ private void reset() { assert Thread.holdsLock(this); if (size < data.length) { Arrays.fill(data, 0, size, null); } else { Arrays.fill(data, null); } this.size = 0; } /** * Expands the internal buffer up to the capacity. */ private void grow() { assert Thread.holdsLock(this); final int len = data.length; int newCapacity = len + (len >> 1) + 1; if (newCapacity > capacity || newCapacity < len) { newCapacity = capacity; } assert len != capacity : len; this.data = copyOf(data, newCapacity); } /** * Configures the handler properties from the log manager. * @param props the given mail properties. Maybe null and are never * captured by this handler. * @throws SecurityException if a security manager exists and the * caller does not have LoggingPermission("control"). */ private synchronized void init(final Properties props) { final LogManager manager = LogManagerProperties.getLogManager(); final String p = getClass().getName(); this.mailProps = new Properties(); //See method comments. this.contentTypes = FileTypeMap.getDefaultFileTypeMap(); //Assign any custom error manager first so it can detect all failures. initErrorManager(manager, p); initLevel(manager, p); initFilter(manager, p); initCapacity(manager, p); initAuthenticator(manager, p); initEncoding(manager, p); initFormatter(manager, p); initComparator(manager, p); initPushLevel(manager, p); initPushFilter(manager, p); initSubject(manager, p); initAttachmentFormaters(manager, p); initAttachmentFilters(manager, p); initAttachmentNames(manager, p); if (props == null && manager.getProperty(p.concat(".verify")) != null) { verifySettings(initSession()); } intern(); //Show verify warnings first. } /** * Interns the error manager, formatters, and filters contained in this * handler. The comparator is not interned. This method can only be * called from init after all of formatters and filters are in a constructed * and in a consistent state. * @since JavaMail 1.5.0 */ private void intern() { assert Thread.holdsLock(this); try { Object canidate; Object result; final Map seen = new HashMap(); try { intern(seen, super.getErrorManager()); } catch (final SecurityException se) { reportError(se.getMessage(), se, ErrorManager.OPEN_FAILURE); } try { canidate = super.getFilter(); result = intern(seen, canidate); if (result != canidate) { super.setFilter(Filter.class.cast(result)); } canidate = super.getFormatter(); result = intern(seen, canidate); if (result != canidate) { super.setFormatter(Formatter.class.cast(result)); } } catch (final SecurityException se) { reportError(se.getMessage(), se, ErrorManager.OPEN_FAILURE); } canidate = this.subjectFormatter; result = intern(seen, canidate); if (result != canidate) { this.subjectFormatter = Formatter.class.cast(result); } canidate = this.pushFilter; result = intern(seen, canidate); if (result != canidate) { this.pushFilter = Filter.class.cast(result); } for (int i = 0; i < attachmentFormatters.length; ++i) { canidate = attachmentFormatters[i]; result = intern(seen, canidate); if (result != canidate) { attachmentFormatters[i] = Formatter.class.cast(result); } canidate = attachmentFilters[i]; result = intern(seen, canidate); if (result != canidate) { attachmentFilters[i] = Filter.class.cast(result); } canidate = attachmentNames[i]; result = intern(seen, canidate); if (result != canidate) { attachmentNames[i] = Formatter.class.cast(result); } } } catch (final Exception skip) { reportError(skip.getMessage(), skip, ErrorManager.OPEN_FAILURE); } } /** * If possible performs an intern of the given object into the * map. If the object can not be interned the given object is returned. * @param m the map used to record the interned values. * @param o the object to try an intern. * @return the original object or an intern replacement. * @throws SecurityException if this operation is not allowed by the * security manager. * @throws Exception if there is an unexpected problem. * @since JavaMail 1.5.0 */ private Object intern(Map m, Object o) throws Exception { if (o == null) { return null; } /** * The common case is that most objects will not intern. The given * object has a public no argument constructor or is an instance of a * TailNameFormatter. TailNameFormatter is safe use as a map key. * For everything else we create a clone of the given object. * This is done because of the following: * 1. Clones can be used to test that a class provides an equals method * and that the equals method works correctly. * 2. Calling equals on the given object is assumed to be cheap. * 3. The intern map can be filtered so it only contains objects that * can be interned, which reduces the memory footprint. * 4. Clones are method local garbage. * 5. Hash code is only called on the clones so bias locking is not * disabled on the objects the handler will use. */ final Object key; if (o.getClass().getName().equals(TailNameFormatter.class.getName())) { key = o; } else { //This call was already made in the LogManagerProperties so this //shouldn't trigger loading of any lazy reflection code. key = o.getClass().getConstructor().newInstance(); } final Object use; //Check the classloaders of each object avoiding the security manager. if (key.getClass() == o.getClass()) { Object found = m.get(key); //Transitive equals test. if (found == null) { //Ensure that equals is symmetric to prove intern is safe. final boolean right = key.equals(o); final boolean left = o.equals(key); if (right && left) { //Assume hashCode is defined at this point. found = m.put(o, o); if (found != null) { reportNonDiscriminating(key, found); found = m.remove(key); if (found != o) { reportNonDiscriminating(key, found); m.clear(); //Try to restore order. } } } else { if (right != left) { reportNonSymmetric(o, key); } } use = o; } else { //Check for a discriminating equals method. if (o.getClass() == found.getClass()) { use = found; } else { reportNonDiscriminating(o, found); use = o; } } } else { use = o; } return use; } /** * Checks a string value for null or empty. * @param s the string. * @return true if the given string is null or zero length. */ private static boolean isEmpty(final String s) { return s == null || s.length() == 0; } /** * Checks that a string is not empty and not equal to the literal "null". * @param name the string to check for a value. * @return true if the string has a valid value. */ private static boolean hasValue(final String name) { return !isEmpty(name) && !"null".equalsIgnoreCase(name); } /** * Parses LogManager string values into objects used by this handler. * @param manager the manager. * @param p the handler class name used as the prefix. * @throws NullPointerException if either argument is null. * @throws SecurityException if not allowed. */ private void initAttachmentFilters(LogManager manager, String p) { assert Thread.holdsLock(this); assert this.attachmentFormatters != null; final String list = manager.getProperty(p.concat(".attachment.filters")); if (!isEmpty(list)) { final String[] names = list.split(","); Filter[] a = new Filter[names.length]; for (int i = 0; i < a.length; ++i) { names[i] = names[i].trim(); if (!"null".equalsIgnoreCase(names[i])) { try { a[i] = LogManagerProperties.newFilter(names[i]); } catch (final SecurityException SE) { throw SE; //Avoid catch all. } catch (final Exception E) { reportError(E.getMessage(), E, ErrorManager.OPEN_FAILURE); } } } this.attachmentFilters = a; if (fixUpAttachmentFilters()) { reportError("Attachment filters.", attachmentMismatch("Length mismatch."), ErrorManager.OPEN_FAILURE); } } else { this.attachmentFilters = emptyFilterArray(); fixUpAttachmentFilters(); } } /** * Parses LogManager string values into objects used by this handler. * @param manager the manager. * @param p the handler class name used as the prefix. * @throws NullPointerException if either argument is null. * @throws SecurityException if not allowed. */ private void initAttachmentFormaters(LogManager manager, String p) { assert Thread.holdsLock(this); final String list = manager.getProperty(p.concat(".attachment.formatters")); if (!isEmpty(list)) { final Formatter[] a; final String[] names = list.split(","); if (names.length == 0) { a = emptyFormatterArray(); } else { a = new Formatter[names.length]; } for (int i = 0; i < a.length; ++i) { names[i] = names[i].trim(); if (!"null".equalsIgnoreCase(names[i])) { try { a[i] = LogManagerProperties.newFormatter(names[i]); if (a[i] instanceof TailNameFormatter) { final Exception CNFE = new ClassNotFoundException(a[i].toString()); reportError("Attachment formatter.", CNFE, ErrorManager.OPEN_FAILURE); a[i] = new SimpleFormatter(); } } catch (final SecurityException SE) { throw SE; //Avoid catch all. } catch (final Exception E) { reportError(E.getMessage(), E, ErrorManager.OPEN_FAILURE); a[i] = new SimpleFormatter(); } } else { final Exception NPE = new NullPointerException(atIndexMsg(i)); reportError("Attachment formatter.", NPE, ErrorManager.OPEN_FAILURE); a[i] = new SimpleFormatter(); } } this.attachmentFormatters = a; } else { this.attachmentFormatters = emptyFormatterArray(); } } /** * Parses LogManager string values into objects used by this handler. * @param manager the manager. * @param p the handler class name used as the prefix. * @throws NullPointerException if either argument is null. * @throws SecurityException if not allowed. */ private void initAttachmentNames(LogManager manager, String p) { assert Thread.holdsLock(this); assert this.attachmentFormatters != null; final String list = manager.getProperty(p.concat(".attachment.names")); if (!isEmpty(list)) { final String[] names = list.split(","); final Formatter[] a = new Formatter[names.length]; for (int i = 0; i < a.length; ++i) { names[i] = names[i].trim(); if (!"null".equalsIgnoreCase(names[i])) { try { try { a[i] = LogManagerProperties.newFormatter(names[i]); } catch (final ClassNotFoundException literal) { a[i] = new TailNameFormatter(names[i]); } catch (final ClassCastException literal) { a[i] = new TailNameFormatter(names[i]); } } catch (final SecurityException SE) { throw SE; //Avoid catch all. } catch (final Exception E) { reportError(E.getMessage(), E, ErrorManager.OPEN_FAILURE); } } else { final Exception NPE = new NullPointerException(atIndexMsg(i)); reportError("Attachment names.", NPE, ErrorManager.OPEN_FAILURE); } } this.attachmentNames = a; if (fixUpAttachmentNames()) { //Any null indexes are repaired. reportError("Attachment names.", attachmentMismatch("Length mismatch."), ErrorManager.OPEN_FAILURE); } } else { this.attachmentNames = emptyFormatterArray(); fixUpAttachmentNames(); } } /** * Parses LogManager string values into objects used by this handler. * @param manager the manager. * @param p the handler class name used as the prefix. * @throws NullPointerException if either argument is null. * @throws SecurityException if not allowed. */ private void initAuthenticator(LogManager manager, String p) { assert Thread.holdsLock(this); String name = manager.getProperty(p.concat(".authenticator")); if (hasValue(name)) { try { this.auth = LogManagerProperties.newAuthenticator(name); } catch (final SecurityException SE) { throw SE; } catch (final ClassNotFoundException literalAuth) { this.auth = new DefaultAuthenticator(name); } catch (final ClassCastException literalAuth) { this.auth = new DefaultAuthenticator(name); } catch (final Exception E) { reportError(E.getMessage(), E, ErrorManager.OPEN_FAILURE); } } } /** * Parses LogManager string values into objects used by this handler. * @param manager the manager. * @param p the handler class name used as the prefix. * @throws NullPointerException if either argument is null. * @throws SecurityException if not allowed. */ private void initLevel(LogManager manager, String p) { assert Thread.holdsLock(this); try { final String val = manager.getProperty(p.concat(".level")); if (val != null) { super.setLevel(Level.parse(val)); } else { super.setLevel(Level.WARNING); } } catch (final SecurityException SE) { throw SE; //Avoid catch all. } catch (final RuntimeException RE) { reportError(RE.getMessage(), RE, ErrorManager.OPEN_FAILURE); try { super.setLevel(Level.WARNING); } catch (final RuntimeException fail) { reportError(fail.getMessage(), fail, ErrorManager.OPEN_FAILURE); } } } /** * Parses LogManager string values into objects used by this handler. * @param manager the manager. * @param p the handler class name used as the prefix. * @throws NullPointerException if either argument is null. * @throws SecurityException if not allowed. */ private void initFilter(LogManager manager, String p) { assert Thread.holdsLock(this); try { String name = manager.getProperty(p.concat(".filter")); if (hasValue(name)) { super.setFilter(LogManagerProperties.newFilter(name)); } } catch (final SecurityException SE) { throw SE; //Avoid catch all. } catch (final Exception E) { reportError(E.getMessage(), E, ErrorManager.OPEN_FAILURE); } } /** * Parses LogManager string values into objects used by this handler. * @param manager the manager. * @param p the handler class name used as the prefix. * @throws NullPointerException if either argument is null. * @throws SecurityException if not allowed. */ private void initCapacity(LogManager manager, String p) { assert Thread.holdsLock(this); final int DEFAULT_CAPACITY = 1000; try { final String value = manager.getProperty(p.concat(".capacity")); if (value != null) { this.setCapacity0(Integer.parseInt(value)); } else { this.setCapacity0(DEFAULT_CAPACITY); } } catch (final RuntimeException RE) { reportError(RE.getMessage(), RE, ErrorManager.OPEN_FAILURE); } if (capacity <= 0) { capacity = DEFAULT_CAPACITY; } this.data = new LogRecord[1]; } /** * Parses LogManager string values into objects used by this handler. * @param manager the manager. * @param p the handler class name used as the prefix. * @throws NullPointerException if either argument is null. * @throws SecurityException if not allowed. */ private void initEncoding(LogManager manager, String p) { assert Thread.holdsLock(this); try { super.setEncoding(manager.getProperty(p.concat(".encoding"))); } catch (final SecurityException SE) { throw SE; //Avoid catch all. } catch (final UnsupportedEncodingException UEE) { reportError(UEE.getMessage(), UEE, ErrorManager.OPEN_FAILURE); } catch (final RuntimeException RE) { reportError(RE.getMessage(), RE, ErrorManager.OPEN_FAILURE); } } /** * Parses LogManager string values into objects used by this handler. * @param manager the manager. * @param p the handler class name used as the prefix. * @throws NullPointerException if either argument is null. * @throws SecurityException if not allowed. */ private void initErrorManager(LogManager manager, String p) { assert Thread.holdsLock(this); String name = manager.getProperty(p.concat(".errorManager")); if (name != null) { try { ErrorManager em = LogManagerProperties.newErrorManager(name); super.setErrorManager(em); } catch (final SecurityException SE) { throw SE; //Avoid catch all. } catch (final Exception E) { reportError(E.getMessage(), E, ErrorManager.OPEN_FAILURE); } } } /** * Parses LogManager string values into objects used by this handler. * @param manager the manager. * @param p the handler class name used as the prefix. * @throws NullPointerException if either argument is null. * @throws SecurityException if not allowed. */ private void initFormatter(LogManager manager, String p) { assert Thread.holdsLock(this); String name = manager.getProperty(p.concat(".formatter")); if (hasValue(name)) { try { final Formatter formatter = LogManagerProperties.newFormatter(name); assert formatter != null; if (formatter instanceof TailNameFormatter == false) { super.setFormatter(formatter); } else { super.setFormatter(new SimpleFormatter()); } } catch (final SecurityException SE) { throw SE; //Avoid catch all. } catch (final Exception E) { reportError(E.getMessage(), E, ErrorManager.OPEN_FAILURE); try { super.setFormatter(new SimpleFormatter()); } catch (final RuntimeException fail) { reportError(fail.getMessage(), fail, ErrorManager.OPEN_FAILURE); } } } else { super.setFormatter(new SimpleFormatter()); } } /** * Parses LogManager string values into objects used by this handler. * @param manager the manager. * @param p the handler class name used as the prefix. * @throws NullPointerException if either argument is null. * @throws SecurityException if not allowed. */ private void initComparator(LogManager manager, String p) { assert Thread.holdsLock(this); String name = manager.getProperty(p.concat(".comparator")); String reverse = manager.getProperty(p.concat(".comparator.reverse")); try { if (hasValue(name)) { comparator = LogManagerProperties.newComparator(name); if (Boolean.parseBoolean(reverse)) { assert comparator != null : "null"; comparator = LogManagerProperties.reverseOrder(comparator); } } else { if (!isEmpty(reverse)) { throw new IllegalArgumentException( "No comparator to reverse."); } } } catch (final SecurityException SE) { throw SE; //Avoid catch all. } catch (final Exception E) { reportError(E.getMessage(), E, ErrorManager.OPEN_FAILURE); } } /** * Parses LogManager string values into objects used by this handler. * @param manager the manager. * @param p the handler class name used as the prefix. * @throws NullPointerException if either argument is null. * @throws SecurityException if not allowed. */ private void initPushLevel(LogManager manager, String p) { assert Thread.holdsLock(this); try { final String val = manager.getProperty(p.concat(".pushLevel")); if (val != null) { this.pushLevel = Level.parse(val); } } catch (final RuntimeException RE) { reportError(RE.getMessage(), RE, ErrorManager.OPEN_FAILURE); } if (this.pushLevel == null) { this.pushLevel = Level.OFF; } } /** * Parses LogManager string values into objects used by this handler. * @param manager the manager. * @param p the handler class name used as the prefix. * @throws NullPointerException if either argument is null. * @throws SecurityException if not allowed. */ private void initPushFilter(LogManager manager, String p) { assert Thread.holdsLock(this); String name = manager.getProperty(p.concat(".pushFilter")); if (hasValue(name)) { try { this.pushFilter = LogManagerProperties.newFilter(name); } catch (final SecurityException SE) { throw SE; //Avoid catch all. } catch (final Exception E) { reportError(E.getMessage(), E, ErrorManager.OPEN_FAILURE); } } } /** * Parses LogManager string values into objects used by this handler. * @param manager the manager. * @param p the handler class name used as the prefix. * @throws NullPointerException if either argument is null. * @throws SecurityException if not allowed. */ private void initSubject(LogManager manager, final String p) { assert Thread.holdsLock(this); String name = manager.getProperty(p.concat(".subject")); if (hasValue(name)) { try { this.subjectFormatter = LogManagerProperties.newFormatter(name); } catch (final SecurityException SE) { throw SE; //Avoid catch all. } catch (final ClassNotFoundException literalSubject) { this.subjectFormatter = new TailNameFormatter(name); } catch (final ClassCastException literalSubject) { this.subjectFormatter = new TailNameFormatter(name); } catch (final Exception E) { this.subjectFormatter = new TailNameFormatter(name); reportError(E.getMessage(), E, ErrorManager.OPEN_FAILURE); } } else { if (name != null) { this.subjectFormatter = new TailNameFormatter(name); } } if (this.subjectFormatter == null) { //Ensure not null. this.subjectFormatter = new TailNameFormatter(""); } } /** * Check if any attachment would actually format the given * LogRecord. This method does not check if the handler * is level is set to OFF or if the handler is closed. * @param record a LogRecord * @return true if the LogRecord would be formatted. */ private boolean isAttachmentLoggable(final LogRecord record) { final Filter[] filters = readOnlyAttachmentFilters(); for (int i = 0; i < filters.length; ++i) { final Filter f = filters[i]; if (f == null || f.isLoggable(record)) { return true; } } return false; } /** * Check if this Handler would push after storing the * LogRecord into its internal buffer. * @param record a LogRecord * @return true if the LogRecord triggers an email push. */ private boolean isPushable(final LogRecord record) { assert Thread.holdsLock(this); final int value = getPushLevel().intValue(); if (value == offValue || record.getLevel().intValue() < value) { return false; } final Filter filter = getPushFilter(); return filter == null || filter.isLoggable(record); } /** * Used to perform push or flush. * @param priority true for high priority otherwise false for normal. * @param code the error manager code. */ private void push(final boolean priority, final int code) { if (tryMutex()) { try { final Message msg = writeLogRecords(code); if (msg != null) { send(msg, priority, code); } } finally { releaseMutex(); } } else { reportUnPublishedError(null); } } /** * Used to send the generated email or write its contents to the * error manager for this handler. This method does not hold any * locks so new records can be added to this handler during a send or * failure. * @param msg the message or null. * @param priority true for high priority or false for normal. * @param code the ErrorManager code. * @throws NullPointerException if message is null. */ private void send(Message msg, boolean priority, int code) { try { envelopeFor(msg, priority); Transport.send(msg); //Calls save changes. } catch (final Exception E) { reportError(msg, E, code); } } /** * Performs a sort on the records if needed. * Any exception thrown during a sort is considered a formatting error. */ private void sort() { assert Thread.holdsLock(this); if (comparator != null) { try { if (size != 1) { Arrays.sort(data, 0, size, comparator); } else { comparator.compare(data[0], data[0]); } } catch (final RuntimeException RE) { reportError(RE.getMessage(), RE, ErrorManager.FORMAT_FAILURE); } } } /** * Formats all records in the buffer and places the output in a Message. * This method under most conditions will catch, report, and continue when * exceptions occur. This method holds a lock on this handler. * @param code the error manager code. * @return null if there are no records or is currently in a push. * Otherwise a new message is created with a formatted message and * attached session. */ private synchronized Message writeLogRecords(final int code) { if (size == 0 || isWriting) { return null; } isWriting = true; try { sort(); if (session == null) { initSession(); } MimeMessage msg = new MimeMessage(session); msg.setDescription(descriptionFrom(comparator, pushLevel, pushFilter)); /** * Parts are lazily created when an attachment performs a getHead * call. Therefore, a null part at an index means that the head is * required. */ MimeBodyPart[] parts = new MimeBodyPart[attachmentFormatters.length]; /** * The buffers are lazily created when the part requires a getHead. */ StringBuilder[] buffers = new StringBuilder[parts.length]; String contentType = null; StringBuilder buf = null; appendSubject(msg, head(subjectFormatter)); final MimeBodyPart body = createBodyPart(); final Formatter bodyFormat = getFormatter(); final Filter bodyFilter = getFilter(); Locale lastLocale = null; for (int ix = 0; ix < size; ++ix) { boolean formatted = false; final LogRecord r = data[ix]; data[ix] = null; //Clear while formatting. final Locale locale = localeFor(r); appendSubject(msg, format(subjectFormatter, r)); if (bodyFilter == null || bodyFilter.isLoggable(r)) { if (buf == null) { buf = new StringBuilder(); final String head = head(bodyFormat); buf.append(head); contentType = contentTypeOf(head); } formatted = true; buf.append(format(bodyFormat, r)); if (locale != null && !locale.equals(lastLocale)) { appendContentLang(body, locale); } } for (int i = 0; i < parts.length; ++i) { final Filter af = attachmentFilters[i]; if (af == null || af.isLoggable(r)) { if (parts[i] == null) { parts[i] = createBodyPart(i); buffers[i] = new StringBuilder(); buffers[i].append(head(attachmentFormatters[i])); appendFileName(parts[i], head(attachmentNames[i])); } formatted = true; appendFileName(parts[i], format(attachmentNames[i], r)); buffers[i].append(format(attachmentFormatters[i], r)); if (locale != null && !locale.equals(lastLocale)) { appendContentLang(parts[i], locale); } } } if (formatted) { if (locale != null && !locale.equals(lastLocale)) { appendContentLang(msg, locale); } } else { //Belongs to no mime part. reportFilterError(r); } lastLocale = locale; } this.size = 0; for (int i = parts.length - 1; i >= 0; --i) { if (parts[i] != null) { appendFileName(parts[i], tail(attachmentNames[i], "err")); buffers[i].append(tail(attachmentFormatters[i], "")); if (buffers[i].length() > 0) { String name = parts[i].getFileName(); if (isEmpty(name)) { //Exceptional case. name = toString(attachmentFormatters[i]); parts[i].setFileName(name); } setContent(parts[i], buffers[i], getContentType(name)); } else { setIncompleteCopy(msg); parts[i] = null; //Skip this part. } buffers[i] = null; } } if (buf != null) { buf.append(tail(bodyFormat, "")); //This body part is always added, even if the buffer is empty, //so the body is never considered an incomplete-copy. } else { buf = new StringBuilder(0); } appendSubject(msg, tail(subjectFormatter, "")); MimeMultipart multipart = new MimeMultipart(); String altType = getContentType(bodyFormat.getClass().getName()); setContent(body, buf, altType == null ? contentType : altType); multipart.addBodyPart(body); for (int i = 0; i < parts.length; ++i) { if (parts[i] != null) { multipart.addBodyPart(parts[i]); } } msg.setContent(multipart); return msg; } catch (final RuntimeException re) { reportError(re.getMessage(), re, code); } catch (final Exception e) { reportError(e.getMessage(), e, code); } finally { isWriting = false; if (size > 0) { reset(); } } return null; } /** * Checks all of the settings if the caller requests a verify and a verify * was not performed yet and no verify is in progress. A verify is * performed on create because this handler may be at the end of a handler * chain and therefore may not see any log records until LogManager.reset() * is called and at that time all of the settings have been cleared. * @param session the current session or null. * @since JavaMail 1.4.4 */ private void verifySettings(final Session session) { if (session != null) { final Properties props = session.getProperties(); final Object check = props.put("verify", ""); if (check instanceof String) { String value = (String) check; //Perform the verify if needed. if (hasValue(value)) { verifySettings0(session, value); } } else { if (check != null) { //This call will fail. verifySettings0(session, check.getClass().toString()); } } } } /** * Checks all of the settings using the given setting. * This triggers the LogManagerProperties to copy all of the mail * settings without explictly knowing them. Once all of the properties * are copied this handler can handle LogManager.reset clearing all of the * properties. It is expected that this method is, at most, only called * once per session. * @param session the current session. * @param verify the type of verify to perform. * @since JavaMail 1.4.4 */ private void verifySettings0(Session session, String verify) { assert verify != null : (String) null; if (!"local".equals(verify) && !"remote".equals(verify) && !"limited".equals(verify) && !"resolve".equals(verify)) { reportError("Verify must be 'limited', local', " + "'resolve' or 'remote'.", new IllegalArgumentException(verify), ErrorManager.OPEN_FAILURE); return; } final MimeMessage abort = new MimeMessage(session); final String msg; if (!"limited".equals(verify)) { msg = "Local address is " + InternetAddress.getLocalAddress(session) + '.'; try { //Verify subclass or declared mime charset. Charset.forName(getEncodingName()); } catch (final RuntimeException RE) { UnsupportedEncodingException UEE = new UnsupportedEncodingException(RE.toString()); UEE.initCause(RE); reportError(msg, UEE, ErrorManager.FORMAT_FAILURE); } } else { msg = "Skipping local address check."; } //Perform all of the copy actions first. synchronized (this) { //Create the subject. appendSubject(abort, head(subjectFormatter)); appendSubject(abort, tail(subjectFormatter, "")); } setIncompleteCopy(abort); //Original body part is never added. envelopeFor(abort, true); try { abort.saveChanges(); } catch (final MessagingException ME) { reportError(msg, ME, ErrorManager.FORMAT_FAILURE); } try { //Ensure transport provider is installed. Address[] all = abort.getAllRecipients(); if (all == null) { //Don't pass null to sendMessage. all = new InternetAddress[0]; } Transport t; try { final Address[] any = all.length != 0 ? all : abort.getFrom(); if (any != null && any.length != 0) { t = session.getTransport(any[0]); session.getProperty("mail.transport.protocol"); //Force copy } else { MessagingException me = new MessagingException( "No recipient or from address."); reportError(msg, me, ErrorManager.OPEN_FAILURE); throw me; } } catch (final MessagingException protocol) { try { t = session.getTransport(); } catch (final MessagingException fail) { throw attach(protocol, fail); } } String local = null; if ("remote".equals(verify)) { MessagingException closed = null; t.connect(); try { try { //Capture localhost while connection is open. if (t instanceof SMTPTransport) { local = ((SMTPTransport) t).getLocalHost(); } //A message without content will fail at message writeTo //when sendMessage is called. This allows the handler //to capture all mail properties set in the LogManager. t.sendMessage(abort, all); } finally { try { //Close the transport before reporting errors. t.close(); } catch (final MessagingException ME) { closed = ME; } } reportUnexpectedSend(abort, verify, null); } catch (final SendFailedException sfe) { Address[] recip = sfe.getInvalidAddresses(); if (recip != null && recip.length != 0) { fixUpContent(abort, verify, sfe); reportError(abort, sfe, ErrorManager.OPEN_FAILURE); } recip = sfe.getValidSentAddresses(); if (recip != null && recip.length != 0) { reportUnexpectedSend(abort, verify, sfe); } } catch (final MessagingException ME) { if (!isMissingContent(abort, ME)) { fixUpContent(abort, verify, ME); reportError(abort, ME, ErrorManager.OPEN_FAILURE); } } if (closed != null) { fixUpContent(abort, verify, closed); reportError(abort, closed, ErrorManager.CLOSE_FAILURE); } } else { //Force a property copy. final String protocol = t.getURLName().getProtocol(); session.getProperty("mail.host"); session.getProperty("mail.user"); session.getProperty("mail." + protocol + ".host"); session.getProperty("mail." + protocol + ".port"); session.getProperty("mail." + protocol + ".user"); local = session.getProperty("mail." + protocol + ".localhost"); if (isEmpty(local)) { local = session.getProperty("mail." + protocol + ".localaddress"); } if ("resolve".equals(verify)) { try { //Resolve the remote host name. verifyHost(t.getURLName().getHost()); } catch (final IOException IOE) { MessagingException ME = new MessagingException(msg, IOE); fixUpContent(abort, verify, ME); reportError(abort, ME, ErrorManager.OPEN_FAILURE); } catch (final RuntimeException RE) { MessagingException ME = new MessagingException(msg, RE); fixUpContent(abort, verify, RE); reportError(abort, ME, ErrorManager.OPEN_FAILURE); } } } if (!"limited".equals(verify)) { try { //Verify host name and hit the host name cache. if (!"remote".equals(verify) && t instanceof SMTPTransport) { local = ((SMTPTransport) t).getLocalHost(); } verifyHost(local); } catch (final IOException IOE) { MessagingException ME = new MessagingException(msg, IOE); fixUpContent(abort, verify, ME); reportError(abort, ME, ErrorManager.OPEN_FAILURE); } catch (final RuntimeException RE) { MessagingException ME = new MessagingException(msg, RE); fixUpContent(abort, verify, ME); reportError(abort, ME, ErrorManager.OPEN_FAILURE); } try { //Verify that the DataHandler can be loaded. final MimeMultipart multipart = new MimeMultipart(); final MimeBodyPart body = new MimeBodyPart(); body.setDisposition(Part.INLINE); body.setDescription(verify); setAcceptLang(body); setContent(body, "", "text/plain"); multipart.addBodyPart(body); abort.setContent(multipart); abort.saveChanges(); abort.writeTo(new ByteArrayOutputStream(MIN_HEADER_SIZE)); } catch (final IOException IOE) { MessagingException ME = new MessagingException(msg, IOE); fixUpContent(abort, verify, ME); reportError(abort, ME, ErrorManager.FORMAT_FAILURE); } } //Verify all recipients. if (all.length != 0) { verifyAddresses(all); } else { throw new MessagingException("No recipient addresses."); } //Verify from and sender addresses. Address[] from = abort.getFrom(); Address sender = abort.getSender(); if (sender instanceof InternetAddress) { ((InternetAddress) sender).validate(); } //If from address is declared then check sender. if (abort.getHeader("From", ",") != null && from.length != 0) { verifyAddresses(from); for (int i = 0; i < from.length; ++i) { if (from[i].equals(sender)) { MessagingException ME = new MessagingException( "Sender address '" + sender + "' equals from address."); throw new MessagingException(msg, ME); } } } else { if (sender == null) { MessagingException ME = new MessagingException( "No from or sender address."); throw new MessagingException(msg, ME); } } //Verify reply-to addresses. verifyAddresses(abort.getReplyTo()); } catch (final MessagingException ME) { fixUpContent(abort, verify, ME); reportError(abort, ME, ErrorManager.OPEN_FAILURE); } catch (final RuntimeException RE) { fixUpContent(abort, verify, RE); reportError(abort, RE, ErrorManager.OPEN_FAILURE); } } /** * Perform a lookup of the host address or FQDN. * @param host the host or null. * @return the address. * @throws IOException if the host name is not valid. * @since JavaMail 1.5.0 */ private static InetAddress verifyHost(String host) throws IOException { InetAddress a; if (isEmpty(host)) { a = InetAddress.getLocalHost(); } else { a = InetAddress.getByName(host); } if (a.getCanonicalHostName().length() == 0) { throw new UnknownHostException(); } return a; } /** * Calls validate for every address given. * If the addresses given are null, empty or not an InternetAddress then * the check is skipped. * @param all any address array, null or empty. * @throws AddressException if there is a problem. * @since JavaMail 1.4.5 */ private static void verifyAddresses(Address[] all) throws AddressException { if (all != null) { for (int i = 0; i < all.length; ++i) { final Address a = all[i]; if (a instanceof InternetAddress) { ((InternetAddress) a).validate(); } } } } /** * Reports that an empty content message was sent and should not have been. * @param msg the MimeMessage. * @param verify the verify enum. * @param cause the exception that caused the problem or null. * @since JavaMail 1.4.5 */ private void reportUnexpectedSend(MimeMessage msg, String verify, Exception cause) { final MessagingException write = new MessagingException( "An empty message was sent.", cause); fixUpContent(msg, verify, write); reportError(msg, write, ErrorManager.OPEN_FAILURE); } /** * Creates and sets the message content from the given Throwable. * When verify fails, this method fixes the 'abort' message so that any * created envelope data can be used in the error manager. * @param msg the message with or without content. * @param verify the verify enum. * @param t the throwable or null. * @since JavaMail 1.4.5 */ private void fixUpContent(MimeMessage msg, String verify, Throwable t) { try { //Add content so toRawString doesn't fail. final MimeBodyPart body; final String subjectType; final String msgDesc; synchronized (this) { body = createBodyPart(); msgDesc = descriptionFrom(comparator, pushLevel, pushFilter); subjectType = getClassId(subjectFormatter); } body.setDescription("Formatted using " + (t == null ? Throwable.class.getName() : t.getClass().getName()) + ", filtered with " + verify + ", and named by " + subjectType + '.'); setContent(body, toMsgString(t), "text/plain"); final MimeMultipart multipart = new MimeMultipart(); multipart.addBodyPart(body); msg.setContent(multipart); msg.setDescription(msgDesc); setAcceptLang(msg); msg.saveChanges(); } catch (final MessagingException ME) { reportError("Unable to create body.", ME, ErrorManager.OPEN_FAILURE); } catch (final RuntimeException RE) { reportError("Unable to create body.", RE, ErrorManager.OPEN_FAILURE); } } /** * Used to update the cached session object based on changes in * mail properties or authenticator. * @return the current session or null if no verify is required. */ private Session fixUpSession() { assert Thread.holdsLock(this); final Session settings; if (mailProps.getProperty("verify") != null) { settings = initSession(); assert settings == session; } else { session = null; //Remove old session. settings = null; } return settings; } /** * Creates a session using a proxy properties object. * @return the session that was created and assigned. */ private Session initSession() { assert Thread.holdsLock(this); final String p = getClass().getName(); LogManagerProperties proxy = new LogManagerProperties(mailProps, p); session = Session.getInstance(proxy, auth); return session; } /** * Creates all of the envelope information for a message. * This method is safe to call outside of a lock because the message * provides the safe snapshot of the mail properties. * @param msg the Message to write the envelope information. * @param priority true for high priority. */ private void envelopeFor(Message msg, boolean priority) { setAcceptLang(msg); setFrom(msg); if (!setRecipient(msg, "mail.to", Message.RecipientType.TO)) { setDefaultRecipient(msg, Message.RecipientType.TO); } setRecipient(msg, "mail.cc", Message.RecipientType.CC); setRecipient(msg, "mail.bcc", Message.RecipientType.BCC); setReplyTo(msg); setSender(msg); setMailer(msg); setAutoSubmitted(msg); if (priority) { setPriority(msg); } try { msg.setSentDate(new java.util.Date()); } catch (final MessagingException ME) { reportError(ME.getMessage(), ME, ErrorManager.FORMAT_FAILURE); } } /** * Factory to create the in-line body part. * @return a body part with default headers set. * @throws MessagingException if there is a problem. */ private MimeBodyPart createBodyPart() throws MessagingException { assert Thread.holdsLock(this); final MimeBodyPart part = new MimeBodyPart(); part.setDisposition(Part.INLINE); part.setDescription(descriptionFrom(getFormatter(), getFilter(), subjectFormatter)); setAcceptLang(part); return part; } /** * Factory to create the attachment body part. * @param index the attachment index. * @return a body part with default headers set. * @throws MessagingException if there is a problem. * @throws IndexOutOfBoundsException if the given index is not an valid * attachment index. */ private MimeBodyPart createBodyPart(int index) throws MessagingException { assert Thread.holdsLock(this); final MimeBodyPart part = new MimeBodyPart(); part.setDisposition(Part.ATTACHMENT); part.setDescription(descriptionFrom( attachmentFormatters[index], attachmentFilters[index], attachmentNames[index])); setAcceptLang(part); return part; } /** * Gets the description for the MimeMessage itself. * The push level and filter are included because they play a role in * formatting of a message when triggered or not triggered. * @param c the comparator. * @param l the pushLevel. * @param f the pushFilter * @return the description. * @throws NullPointerException if level is null. * @since JavaMail 1.4.5 */ private String descriptionFrom(Comparator c, Level l, Filter f) { return "Sorted using "+ (c == null ? "no comparator" : c.getClass().getName()) + ", pushed when "+ l.getName() + ", and " + (f == null ? "no push filter" : f.getClass().getName()) + '.'; } /** * Creates a description for a body part. * @param f the content formatter. * @param filter the content filter. * @param name the naming formatter. * @return the description for the body part. */ private String descriptionFrom(Formatter f, Filter filter, Formatter name) { return "Formatted using " + getClassId(f) + ", filtered with " + (filter == null ? "no filter" : filter.getClass().getName()) +", and named by " + getClassId(name) + '.'; } /** * Gets a class name represents the behavior of the formatter. * The class name may not be assignable to a Formatter. * @param f the formatter. * @return a class name that represents the given formatter. * @throws NullPointerException if the parameter is null. * @since JavaMail 1.4.5 */ private String getClassId(final Formatter f) { if (f instanceof TailNameFormatter) { return String.class.getName(); //Literal string. } else { return f.getClass().getName(); } } /** * Ensure that a formatter creates a valid string for a part name. * @param f the formatter. * @return the to string value or the class name. */ private String toString(final Formatter f) { //Should never be null but, guard against formatter bugs. final String name = f.toString(); if (!isEmpty(name)) { return name; } else { return getClassId(f); } } /** * Constructs a file name from a formatter. This method is called often * but, rarely does any work. * @param part to append to. * @param chunk non null string to append. */ private void appendFileName(final Part part, final String chunk) { if (chunk != null) { if (chunk.length() > 0) { appendFileName0(part, chunk); } } else { reportNullError(ErrorManager.FORMAT_FAILURE); } } /** * It is assumed that file names are short and that in most cases * getTail will be the only method that will produce a result. * @param part to append to. * @param chunk non null string to append. */ private void appendFileName0(final Part part, String chunk) { try { //Remove all control character groups. chunk = chunk.replaceAll("[\\x00-\\x1F\\x7F]+", ""); final String old = part.getFileName(); part.setFileName(old != null ? old.concat(chunk) : chunk); } catch (final MessagingException ME) { reportError(ME.getMessage(), ME, ErrorManager.FORMAT_FAILURE); } } /** * Constructs a subject line from a formatter. * @param msg to append to. * @param chunk non null string to append. */ private void appendSubject(final Message msg, final String chunk) { if (chunk != null) { if (chunk.length() > 0) { appendSubject0(msg, chunk); } } else { reportNullError(ErrorManager.FORMAT_FAILURE); } } /** * It is assumed that subject lines are short and that in most cases * getTail will be the only method that will produce a result. * @param msg to append to. * @param chunk non null string to append. */ private void appendSubject0(final Message msg, String chunk) { try { //Remove all control character groups. chunk = chunk.replaceAll("[\\x00-\\x1F\\x7F]+", ""); final String encoding = getEncodingName(); final String old = msg.getSubject(); assert msg instanceof MimeMessage; ((MimeMessage) msg).setSubject(old != null ? old.concat(chunk) : chunk, MimeUtility.mimeCharset(encoding)); } catch (final MessagingException ME) { reportError(ME.getMessage(), ME, ErrorManager.FORMAT_FAILURE); } } /** * Gets the locale for the given log record from the resource bundle. * If the resource bundle is using the root locale then the default locale * is returned. * @param r the log record. * @return null if not localized otherwise, the locale of the record. * @since JavaMail 1.4.5 */ private Locale localeFor(final LogRecord r) { Locale l; final ResourceBundle rb = r.getResourceBundle(); if (rb != null) { l = rb.getLocale(); if (l == null || isEmpty(l.getLanguage())) { //The language of the fallback bundle (root) is unknown. //1. Use default locale. Should only be wrong if the app is // used with a langauge that was unintended. (unlikely) //2. Mark it as not localized (force null, info loss). //3. Use the bundle name (encoded) as an experimental language. l = Locale.getDefault(); } } else { l = null; } return l; } /** * Appends the content language to the given mime part. * The language tag is only appended if the given language has not been * specified. This method is only used when we have LogRecords that are * localized with an assigned resource bundle. * @param p the mime part. * @param l the locale to append. * @throws NullPointerException if any argument is null. * @since JavaMail 1.4.5 */ private void appendContentLang(final MimePart p, final Locale l) { try { String lang = LogManagerProperties.toLanguageTag(l); if (lang.length() != 0) { String header = p.getHeader("Content-Language", null); if (isEmpty(header)) { p.setHeader("Content-Language", lang); } else if (!header.equalsIgnoreCase(lang)) { lang = ",".concat(lang); int idx = 0; while ((idx = header.indexOf(lang, idx)) > -1) { idx += lang.length(); if (idx == header.length() || header.charAt(idx) == ',') { break; } } if (idx < 0) { int len = header.lastIndexOf("\r\n\t"); if (len < 0) { //If not folded. len = (18 + 2) + header.length(); } else { len = (header.length() - len) + 8; } //Perform folding of header if needed. if ((len + lang.length()) > 76) { header = header.concat("\r\n\t".concat(lang)); } else { header = header.concat(lang); } p.setHeader("Content-Language", header); } } } } catch (final MessagingException ME) { reportError(ME.getMessage(), ME, ErrorManager.FORMAT_FAILURE); } } /** * Sets the accept language to the default locale of the JVM. * If the locale is the root locale the header is not added. * @param p the part to set. * @since JavaMail 1.4.5 */ private void setAcceptLang(final Part p) { try { final String lang = LogManagerProperties .toLanguageTag(Locale.getDefault()); if (lang.length() != 0) { p.setHeader("Accept-Language", lang); } } catch (final MessagingException ME) { reportError(ME.getMessage(), ME, ErrorManager.FORMAT_FAILURE); } } /** * Used when a log record was loggable prior to being inserted * into the buffer but at the time of formatting was no longer loggable. * Filters were changed after publish but prior to a push or a bug in the * body filter or one of the attachment filters. * @param record that was not formatted. * @since JavaMail 1.4.5 */ private void reportFilterError(final LogRecord record) { assert Thread.holdsLock(this); final SimpleFormatter f = new SimpleFormatter(); final String msg = "Log record " + record.getSequenceNumber() + " was filtered from all message parts. " + head(f) + format(f, record) + tail(f, ""); final String txt = getFilter() + ", " + Arrays.asList(readOnlyAttachmentFilters()); reportError(msg, new IllegalArgumentException(txt), ErrorManager.FORMAT_FAILURE); } /** * Reports symmetric contract violations an equals implementation. * @param o the test object must be non null. * @param found the possible intern, must be non null. * @throws NullPointerException if any argument is null. * @since JavaMail 1.5.0 */ private void reportNonSymmetric(final Object o, final Object found) { reportError("Non symmetric equals implementation." , new IllegalArgumentException(o.getClass().getName() + " is not equal to " + found.getClass().getName()) , ErrorManager.OPEN_FAILURE); } /** * Reports equals implementations that do not discriminate between objects * of different types or subclass types. * @param o the test object must be non null. * @param found the possible intern, must be non null. * @throws NullPointerException if any argument is null. * @since JavaMail 1.5.0 */ private void reportNonDiscriminating(final Object o, final Object found) { reportError("Non discriminating equals implementation." , new IllegalArgumentException(o.getClass().getName() + " should not be equal to " + found.getClass().getName()) , ErrorManager.OPEN_FAILURE); } /** * Used to outline the bytes to report a null pointer exception. * See BUD ID 6533165. * @param code the ErrorManager code. */ private void reportNullError(final int code) { reportError("null", new NullPointerException(), code); } /** * Creates the head or reports a formatting error. * @param f the formatter. * @return the head string or an empty string. */ private String head(final Formatter f) { try { return f.getHead(this); } catch (final RuntimeException RE) { reportError(RE.getMessage(), RE, ErrorManager.FORMAT_FAILURE); return ""; } } /** * Creates the formatted log record or reports a formatting error. * @param f the formatter. * @param r the log record. * @return the formatted string or an empty string. */ private String format(final Formatter f, final LogRecord r) { try { return f.format(r); } catch (final RuntimeException RE) { reportError(RE.getMessage(), RE, ErrorManager.FORMAT_FAILURE); return ""; } } /** * Creates the tail or reports a formatting error. * @param f the formatter. * @param def the default string to use when there is an error. * @return the tail string or the given default string. */ private String tail(final Formatter f, final String def) { try { return f.getTail(this); } catch (final RuntimeException RE) { reportError(RE.getMessage(), RE, ErrorManager.FORMAT_FAILURE); return def; } } /** * Sets the x-mailer header. * @param msg the target message. */ private void setMailer(final Message msg) { try { final Class mail = MailHandler.class; final Class k = getClass(); String value; if (k == mail) { value = mail.getName(); } else { try { value = MimeUtility.encodeText(k.getName()); } catch (final UnsupportedEncodingException E) { reportError(E.getMessage(), E, ErrorManager.FORMAT_FAILURE); value = k.getName().replaceAll("[^\\x00-\\x7F]", "\uu001A"); } value = MimeUtility.fold(10, mail.getName() + " using the " + value + " extension."); } msg.setHeader("X-Mailer", value); } catch (final MessagingException ME) { reportError(ME.getMessage(), ME, ErrorManager.FORMAT_FAILURE); } } /** * Sets the priority and importance headers. * @param msg the target message. */ private void setPriority(final Message msg) { try { msg.setHeader("Importance", "High"); msg.setHeader("Priority", "urgent"); msg.setHeader("X-Priority", "2"); //High } catch (final MessagingException ME) { reportError(ME.getMessage(), ME, ErrorManager.FORMAT_FAILURE); } } /** * Used to signal that body parts are missing from a message. Also used * when LogRecords were passed to an attachment formatter but the formatter * produced no output, which is allowed. Used during a verify because all * parts are omitted, none of the content formatters are used. This is * not used when a filter prevents LogRecords from being formatted. * This header is defined in RFC 2156 and RFC 4021. * @param msg the message. * @since JavaMail 1.4.5 */ private void setIncompleteCopy(final Message msg) { try { msg.setHeader("Incomplete-Copy", ""); } catch (final MessagingException ME) { reportError(ME.getMessage(), ME, ErrorManager.FORMAT_FAILURE); } } /** * Signals that this message was generated by automatic process. * This header is defined in RFC 3834 section 5. * @param msg the message. * @since JavaMail 1.4.6 */ private void setAutoSubmitted(final Message msg) { try { msg.setHeader("auto-submitted", "auto-generated"); //RFC 3834 (5.2) } catch (final MessagingException ME) { reportError(ME.getMessage(), ME, ErrorManager.FORMAT_FAILURE); } } /** * Sets from address header. * @param msg the target message. */ private void setFrom(final Message msg) { final String from = msg.getSession().getProperty("mail.from"); if (from != null) { try { final Address[] address = InternetAddress.parse(from, false); if (address.length > 0) { if (address.length == 1) { msg.setFrom(address[0]); } else { //Greater than 1 address. msg.addFrom(address); } } //Can't place an else statement here because the 'from' is //not null which causes the local address computation //to fail. Assume the user wants to omit the from address //header. } catch (final MessagingException ME) { reportError(ME.getMessage(), ME, ErrorManager.FORMAT_FAILURE); setDefaultFrom(msg); } } else { setDefaultFrom(msg); } } /** * Sets the from header to the local address. * @param msg the target message. */ private void setDefaultFrom(final Message msg) { try { msg.setFrom(); } catch (final MessagingException ME) { reportError(ME.getMessage(), ME, ErrorManager.FORMAT_FAILURE); } } /** * Computes the default to-address if none was specified. This can * fail if the local address can't be computed. * @param msg the message * @param type the recipient type. * @since JavaMail 1.5.0 */ private void setDefaultRecipient(final Message msg, final Message.RecipientType type) { try { Address a = InternetAddress.getLocalAddress(msg.getSession()); if (a != null) { msg.setRecipient(type, a); } else { final MimeMessage m = new MimeMessage(msg.getSession()); m.setFrom(); //Should throw an exception with a cause. Address[] from = m.getFrom(); if (from.length > 0) { msg.setRecipients(type, from); } else { throw new MessagingException("No local address."); } } } catch (final MessagingException ME) { reportError("Unable to compute a default recipient.", ME, ErrorManager.FORMAT_FAILURE); } catch (final RuntimeException RE) { reportError("Unable to compute a default recipient.", RE, ErrorManager.FORMAT_FAILURE); } } /** * Sets reply-to address header. * @param msg the target message. */ private void setReplyTo(final Message msg) { final String reply = msg.getSession().getProperty("mail.reply.to"); if (!isEmpty(reply)) { try { final Address[] address = InternetAddress.parse(reply, false); if (address.length > 0) { msg.setReplyTo(address); } } catch (final MessagingException ME) { reportError(ME.getMessage(), ME, ErrorManager.FORMAT_FAILURE); } } } /** * Sets sender address header. * @param msg the target message. */ private void setSender(final Message msg) { assert msg instanceof MimeMessage : msg; final String sender = msg.getSession().getProperty("mail.sender"); if (!isEmpty(sender)) { try { final InternetAddress[] address = InternetAddress.parse(sender, false); if (address.length > 0) { ((MimeMessage) msg).setSender(address[0]); if (address.length > 1) { reportError("Ignoring other senders.", tooManyAddresses(address, 1), ErrorManager.FORMAT_FAILURE); } } } catch (final MessagingException ME) { reportError(ME.getMessage(), ME, ErrorManager.FORMAT_FAILURE); } } } /** * A common factory used to create the too many addresses exception. * @param address the addresses, never null. * @param offset the starting address to display. * @return the too many addresses exception. */ private AddressException tooManyAddresses(Address[] address, int offset) { Object l = Arrays.asList(address).subList(offset, address.length); return new AddressException(l.toString()); } /** * Sets the recipient for the given message. * @param msg the message. * @param key the key to search in the session. * @param type the recipient type. * @return true if the key was contained in the session. */ private boolean setRecipient(final Message msg, final String key, final Message.RecipientType type) { boolean containsKey; final String value = msg.getSession().getProperty(key); containsKey = value != null; if (!isEmpty(value)) { try { final Address[] address = InternetAddress.parse(value, false); if (address.length > 0) { msg.setRecipients(type, address); } } catch (final MessagingException ME) { reportError(ME.getMessage(), ME, ErrorManager.FORMAT_FAILURE); } } return containsKey; } /** * Converts an email message to a raw string. This raw string * is passed to the error manager to allow custom error managers * to recreate the original MimeMessage object. * @param msg a Message object. * @return the raw string or null if msg was null. * @throws MessagingException if there was a problem with the message. * @throws IOException if there was a problem. */ private String toRawString(final Message msg) throws MessagingException, IOException { if (msg != null) { final int nbytes = Math.max(msg.getSize() + MIN_HEADER_SIZE, MIN_HEADER_SIZE); final ByteArrayOutputStream out = new ByteArrayOutputStream(nbytes); msg.writeTo(out); return out.toString("US-ASCII"); //Raw message is always ASCII. } else { //Must match this.reportError behavior, see push method. return null; //Null is the safe choice. } } /** * Converts a throwable to a message string. * @param t any throwable or null. * @return the throwable with a stack trace or the literal null. */ private String toMsgString(final Throwable t) { if (t == null) { return "null"; } final String encoding = getEncodingName(); try { final ByteArrayOutputStream out = new ByteArrayOutputStream(MIN_HEADER_SIZE); //Create an output stream writer so streams are not double buffered. final PrintWriter pw = new PrintWriter(new OutputStreamWriter(out, encoding)); pw.println(t.getMessage()); t.printStackTrace(pw); pw.flush(); pw.close(); //BUG ID 6995537 return out.toString(encoding); } catch (IOException badMimeCharset) { return t.toString() + ' ' + badMimeCharset.toString(); } } /** * Replaces the current context class loader with our class loader. * @return null for the boot class loader, a class loader, or a marker * object to signal that no modification was required. * @since JavaMail 1.4.6 */ private Object getAndSetContextClassLoader() { try { return AccessController.doPrivileged(GET_AND_SET_CCL); } catch (final SecurityException ignore) { return GET_AND_SET_CCL; //return not modified. } } /** * Restores the original context class loader. * @param ccl null for the boot class loader, a class loader, or a * marker object to signal that no modification is required. * @since JavaMail 1.4.6 */ private void setContextClassLoader(final Object ccl) { //Boot class loader or a new context class loader. if (ccl == null || ccl instanceof ClassLoader) { AccessController.doPrivileged(new GetAndSetContext(ccl)); } } /** * A factory used to create a common attachment mismatch type. * @param msg the exception message. * @return a RuntimeException to represent the type of error. */ private static RuntimeException attachmentMismatch(final String msg) { return new IndexOutOfBoundsException(msg); } /** * Outline the attachment mismatch message. See Bug ID 6533165. * @param expected the expected array length. * @param found the array length that was given. * @return a RuntimeException populated with a message. */ private static RuntimeException attachmentMismatch(int expected, int found) { return attachmentMismatch("Attachments mismatched, expected " + expected + " but given " + found + '.'); } /** * Try to attach a suppressed exception to a MessagingException in any order * that is possible. * @param required the exception expected to see as a reported failure. * @param optional the suppressed exception. * @return either the required or the optional exception. */ private static MessagingException attach( MessagingException required, Exception optional) { if (optional != null && !required.setNextException(optional)) { if (optional instanceof MessagingException) { final MessagingException head = (MessagingException) optional; if (head.setNextException(required)) { return head; } } } return required; } /** * Outline the creation of the index error message. See BUG ID 6533165. * @param i the index. * @return the error message. */ private static String atIndexMsg(final int i) { return "At index: " + i + '.'; } /** * Used for storing a password from the LogManager or literal string. * @since JavaMail 1.4.6 */ private static final class DefaultAuthenticator extends Authenticator { /** * The password to use. */ private final String pass; /** * Creates a new DefaultAuthenticator. * @param pass the password. */ DefaultAuthenticator(final String pass) { assert pass != null; this.pass = pass; } @Override protected final PasswordAuthentication getPasswordAuthentication() { return new PasswordAuthentication(getDefaultUserName(), pass); } } /** * Performs a get and set of the context class loader with privileges * enabled. * @since JavaMail 1.4.6 */ private static final class GetAndSetContext implements PrivilegedAction { /** * The source containing the class loader. */ private final Object source; /** * Create the action. * @param source null for boot class loader, a class loader, a class * used to get the class loader, or a source object to get the class * loader. */ GetAndSetContext(final Object source) { this.source = source; } /** * Gets the class loader from the source and sets the CCL only if * the source and CCL are not the same. * @return the replaced context class loader which can be null or this * to indicate that nothing was modified. */ public final Object run() { final Thread current = Thread.currentThread(); final ClassLoader ccl = current.getContextClassLoader(); final ClassLoader loader; if (source == null) { loader = null; //boot class loader } else if (source instanceof ClassLoader) { loader = (ClassLoader) source; } else if (source instanceof Class) { loader = ((Class) source).getClassLoader(); } else { assert !(source instanceof Class) : source; loader = source.getClass().getClassLoader(); } if (ccl != loader) { current.setContextClassLoader(loader); return ccl; } else { return this; //Unchanged, return non null and non classloader. } } } /** * Used for naming attachment file names and the main subject line. */ private static final class TailNameFormatter extends Formatter { /** * The value used as the output. */ private final String name; /** * Creates the formatter with the given name. * Default access to avoid extra generated class files. * @param name any not null string. */ TailNameFormatter(final String name) { assert name != null; this.name = name; } public final String format(LogRecord record) { return ""; } @Override public final String getTail(Handler h) { return name; } /** * Equals method. * @param o the other object. * @return true if equal * @since JavaMail 1.4.4 */ @Override public final boolean equals(Object o) { if (o instanceof TailNameFormatter) { return name.equals(((TailNameFormatter) o).name); } return false; } /** * Hash code method. * @return the hash code. * @since JavaMail 1.4.4 */ @Override public final int hashCode() { return getClass().hashCode() + name.hashCode(); } @Override public final String toString() { return name; } } }