org.apache.james.server.core.MailImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.james.server.core.MailImpl.java

Source

/****************************************************************
 * Licensed to the Apache Software Foundation (ASF) under one   *
 * or more contributor license agreements.  See the NOTICE file *
 * distributed with this work for additional information        *
 * regarding copyright ownership.  The ASF licenses this file   *
 * to you under the Apache License, Version 2.0 (the            *
 * "License"); you may not use this file except in compliance   *
 * with the License.  You may obtain a copy of the License at   *
 *                                                              *
 *   http://www.apache.org/licenses/LICENSE-2.0                 *
 *                                                              *
 * Unless required by applicable law or agreed to in writing,   *
 * software distributed under the License is distributed on an  *
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
 * KIND, either express or implied.  See the License for the    *
 * specific language governing permissions and limitations      *
 * under the License.                                           *
 ****************************************************************/

package org.apache.james.server.core;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OptionalDataException;
import java.io.OutputStream;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.mail.Address;
import javax.mail.MessagingException;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.ParseException;

import org.apache.commons.lang.RandomStringUtils;
import org.apache.james.core.MailAddress;
import org.apache.james.core.MaybeSender;
import org.apache.james.core.builder.MimeMessageBuilder;
import org.apache.james.lifecycle.api.Disposable;
import org.apache.james.lifecycle.api.LifecycleUtil;
import org.apache.mailet.Attribute;
import org.apache.mailet.AttributeName;
import org.apache.mailet.AttributeValue;
import org.apache.mailet.Mail;
import org.apache.mailet.PerRecipientHeaders;
import org.apache.mailet.PerRecipientHeaders.Header;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.github.fge.lambdas.Throwing;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.primitives.Chars;

/**
 * <p>
 * Wraps a MimeMessage adding routing information (from SMTP) and some simple
 * API enhancements.
 * </p>
 * <p>
 * From James version > 2.2.0a8 "mail attributes" have been added. Backward and
 * forward compatibility is supported:
 * <ul>
 * <li>messages stored in file repositories <i>without</i> attributes by James
 * version <= 2.2.0a8 will be processed by later versions as having an empty
 * attributes hashmap;</li>
 * <li>messages stored in file repositories <i>with</i> attributes by James
 * version > 2.2.0a8 will be processed by previous versions, ignoring the
 * attributes.</li>
 * </ul>
 * </p>
 */
public class MailImpl implements Disposable, Mail {

    /**
     * Create a copy of the input mail and assign it a new name
     *
     * @param mail original mail
     * @throws MessagingException when the message is not clonable
     */
    public static MailImpl duplicate(Mail mail) throws MessagingException {
        return new MailImpl(mail, deriveNewName(mail.getName()));
    }

    public static MailImpl fromMimeMessage(String name, MimeMessage mimeMessage) throws MessagingException {
        MailAddress sender = getSender(mimeMessage);
        ImmutableList<MailAddress> recipients = getRecipients(mimeMessage);
        return new MailImpl(name, sender, recipients, mimeMessage);
    }

    public static Builder builder() {
        return new Builder();
    }

    public static class Builder {

        private Optional<MimeMessage> mimeMessage;
        private List<MailAddress> recipients;
        private Optional<String> name;
        private Optional<MailAddress> sender;
        private Optional<String> state;
        private Optional<String> errorMessage;
        private Optional<Date> lastUpdated;
        private Map<AttributeName, Attribute> attributes;
        private Optional<String> remoteAddr;
        private Optional<String> remoteHost;
        private PerRecipientHeaders perRecipientHeaders;

        private Builder() {
            mimeMessage = Optional.empty();
            recipients = Lists.newArrayList();
            name = Optional.empty();
            sender = Optional.empty();
            state = Optional.empty();
            errorMessage = Optional.empty();
            lastUpdated = Optional.empty();
            attributes = Maps.newHashMap();
            remoteAddr = Optional.empty();
            remoteHost = Optional.empty();
            perRecipientHeaders = new PerRecipientHeaders();
        }

        public Builder mimeMessage(MimeMessage mimeMessage) {
            this.mimeMessage = Optional.ofNullable(mimeMessage);
            return this;
        }

        public Builder mimeMessage(MimeMessageBuilder mimeMessage) throws MessagingException {
            this.mimeMessage = Optional.ofNullable(mimeMessage.build());
            return this;
        }

        public Builder recipients() {
            return this;
        }

        public Builder recipients(List<MailAddress> recipients) {
            this.recipients.addAll(recipients);
            return this;
        }

        public Builder recipients(MailAddress... recipients) {
            return recipients(ImmutableList.copyOf(recipients));
        }

        public Builder recipients(String... recipients) {
            return recipients(Arrays.stream(recipients).map(Throwing.function(MailAddress::new))
                    .collect(ImmutableList.toImmutableList()));
        }

        public Builder recipient(MailAddress recipient) {
            return recipients(recipient);
        }

        public Builder recipient(String recipient) throws AddressException {
            return recipients(recipient);
        }

        public Builder name(String name) {
            this.name = Optional.ofNullable(name);
            return this;
        }

        public Builder sender(MailAddress sender) {
            return sender(Optional.ofNullable(sender));
        }

        public Builder sender(Optional<MailAddress> sender) {
            this.sender = sender;
            return this;
        }

        public Builder sender(MaybeSender sender) {
            this.sender = sender.asOptional();
            return this;
        }

        public Builder sender(String sender) throws AddressException {
            return sender(new MailAddress(sender));
        }

        public Builder state(String state) {
            this.state = Optional.ofNullable(state);
            return this;
        }

        public Builder errorMessage(String errorMessage) {
            this.errorMessage = Optional.ofNullable(errorMessage);
            return this;
        }

        public Builder lastUpdated(Date lastUpdated) {
            this.lastUpdated = Optional.ofNullable(lastUpdated);
            return this;
        }

        @Deprecated
        public Builder attribute(String name, Serializable object) {
            attribute(Attribute.convertToAttribute(name, object));
            return this;
        }

        @Deprecated
        public Builder attributes(Map<String, Serializable> attributes) {
            this.attributes(toAttributeMap(attributes).values());
            return this;
        }

        public Builder attribute(Attribute attribute) {
            this.attributes.put(attribute.getName(), attribute);
            return this;
        }

        public Builder attributes(Collection<Attribute> attributes) {
            this.attributes.putAll(attributes.stream()
                    .collect(ImmutableMap.toImmutableMap(Attribute::getName, Function.identity())));
            return this;
        }

        public Builder remoteAddr(String remoteAddr) {
            this.remoteAddr = Optional.ofNullable(remoteAddr);
            return this;
        }

        public Builder remoteHost(String remoteHost) {
            this.remoteHost = Optional.ofNullable(remoteHost);
            return this;
        }

        public Builder addHeaderForRecipient(Header header, MailAddress recipient) {
            this.perRecipientHeaders.addHeaderForRecipient(header, recipient);
            return this;
        }

        public Builder addAllHeadersForRecipients(PerRecipientHeaders perRecipientHeaders) {
            this.perRecipientHeaders.addAll(perRecipientHeaders);
            return this;
        }

        public MailImpl build() {
            MailImpl mail = new MailImpl();
            mimeMessage.ifPresent(Throwing.consumer(mail::setMessage).sneakyThrow());
            name.ifPresent(mail::setName);
            sender.ifPresent(mail::setSender);
            mail.setRecipients(recipients);
            state.ifPresent(mail::setState);
            errorMessage.ifPresent(mail::setErrorMessage);
            lastUpdated.ifPresent(mail::setLastUpdated);
            mail.setAttributes(attributes);
            remoteAddr.ifPresent(mail::setRemoteAddr);
            remoteHost.ifPresent(mail::setRemoteHost);
            mail.perRecipientSpecificHeaders.addAll(perRecipientHeaders);
            return mail;
        }
    }

    private static Map<AttributeName, Attribute> toAttributeMap(Map<String, ?> attributes) {
        return attributes.entrySet().stream()
                .map(entry -> Attribute.convertToAttribute(entry.getKey(), entry.getValue()))
                .collect(Collectors.toMap(Attribute::getName, Function.identity()));
    }

    private static ImmutableList<MailAddress> getRecipients(MimeMessage mimeMessage) throws MessagingException {
        return Arrays.stream(mimeMessage.getAllRecipients())
                .map(Throwing.function(MailImpl::castToMailAddress).sneakyThrow())
                .collect(ImmutableList.toImmutableList());
    }

    private static MailAddress getSender(MimeMessage mimeMessage) throws MessagingException {
        Address[] sender = mimeMessage.getFrom();
        Preconditions.checkArgument(sender.length == 1);
        return castToMailAddress(sender[0]);
    }

    private static MailAddress castToMailAddress(Address address) throws AddressException {
        Preconditions.checkArgument(address instanceof InternetAddress);
        return new MailAddress((InternetAddress) address);
    }

    /**
     * Create a unique new primary key name for the given MailObject.
     * Detect if this has been called more than 8 times recursively
     *
     * @param currentName the mail to use as the basis for the new mail name
     * @return a new name
     */
    @VisibleForTesting
    static String deriveNewName(String currentName) throws MessagingException {
        char separator = '!';
        int loopThreshold = 7;
        int suffixLength = 9;
        int suffixMaxLength = loopThreshold * suffixLength;
        int nameMaxLength = suffixMaxLength + 13;

        detectPossibleLoop(currentName, loopThreshold, separator);

        // Checking if the original mail name is too long, perhaps because of a
        // loop caused by a configuration error.
        // it could cause a "null pointer exception" in AvalonMailRepository
        // much harder to understand.
        String newName = currentName + generateRandomSuffix(suffixLength, separator);
        return stripFirstCharsIfNeeded(nameMaxLength, newName);
    }

    private static String stripFirstCharsIfNeeded(int nameMaxLength, String newName) {
        return newName.substring(Math.max(0, newName.length() - nameMaxLength));
    }

    private static String generateRandomSuffix(int suffixLength, char separator) {
        return "-" + separator + RandomStringUtils.randomNumeric(suffixLength - 2);
    }

    private static void detectPossibleLoop(String currentName, int loopThreshold, char separator)
            throws MessagingException {
        long occurrences = currentName.chars().filter(c -> Chars.saturatedCast(c) == separator).count();

        // It looks like a configuration loop. It's better to stop.
        if (occurrences > loopThreshold) {
            throw new MessagingException(
                    "Unable to create a new message name: too long. Possible loop in config.xml.");
        }
    }

    private static final Logger LOGGER = LoggerFactory.getLogger(MailImpl.class);

    /**
     * We hardcode the serialVersionUID so that from James 1.2 on, MailImpl will
     * be deserializable (so your mail doesn't get lost)
     */
    public static final long serialVersionUID = -4289663364703986260L;
    /**
     * The error message, if any, associated with this mail.
     */
    private String errorMessage;
    /**
     * The state of this mail, which determines how it is processed.
     */
    private String state;
    /**
     * The MimeMessage that holds the mail data.
     */
    private MimeMessageCopyOnWriteProxy message;
    /**
     * The sender of this mail.
     */
    private MailAddress sender;
    /**
     * The collection of recipients to whom this mail was sent.
     */
    private Collection<MailAddress> recipients;
    /**
     * The identifier for this mail message
     */
    private String name;
    /**
     * The remote host from which this mail was sent.
     */
    private String remoteHost = "localhost";
    /**
     * The remote address from which this mail was sent.
     */
    private String remoteAddr = "127.0.0.1";
    /**
     * The last time this message was updated.
     */
    private Date lastUpdated = new Date();
    /**
     * Attributes added to this MailImpl instance
     */
    private Map<AttributeName, Attribute> attributes;
    /**
     * Specific headers for some recipients
     * These headers will be added at delivery time
     */
    private PerRecipientHeaders perRecipientSpecificHeaders;

    /**
     * A constructor that creates a new, uninitialized MailImpl
     */
    public MailImpl() {
        setState(Mail.DEFAULT);
        attributes = new HashMap<>();
        perRecipientSpecificHeaders = new PerRecipientHeaders();
        this.recipients = null;
    }

    /**
     * A constructor that creates a MailImpl with the specified name, sender,
     * and recipients.
     *
     * @param name       the name of the MailImpl
     * @param sender     the sender for this MailImpl
     * @param recipients the collection of recipients of this MailImpl
     */
    public MailImpl(String name, MailAddress sender, Collection<MailAddress> recipients) {
        this(name, Optional.ofNullable(sender), recipients);
    }

    public MailImpl(String name, Optional<MailAddress> sender, Collection<MailAddress> recipients) {
        this();
        setName(name);
        sender.ifPresent(this::setSender);

        // Copy the recipient list
        if (recipients != null) {
            setRecipients(recipients);
        }
    }

    @SuppressWarnings({ "unchecked", "deprecation" })
    private MailImpl(Mail mail, String newName) throws MessagingException {
        this(newName, mail.getSender(), mail.getRecipients(), mail.getMessage());
        setRemoteHost(mail.getRemoteHost());
        setRemoteAddr(mail.getRemoteAddr());
        setLastUpdated(mail.getLastUpdated());
        setErrorMessage(mail.getErrorMessage());
        try {
            if (mail instanceof MailImpl) {
                setAttributesRaw(
                        (Map<String, Object>) cloneSerializableObject(((MailImpl) mail).getAttributesRaw()));
            } else {
                HashMap<String, Object> attribs = new HashMap<>();
                for (Iterator<String> i = mail.getAttributeNames(); i.hasNext();) {
                    String hashKey = i.next();
                    attribs.put(hashKey, cloneSerializableObject(mail.getAttribute(hashKey)));
                }
                setAttributesRaw(attribs);
            }
        } catch (IOException | ClassNotFoundException e) {
            LOGGER.error("Error while deserializing attributes", e);
            setAttributesRaw(new HashMap<>());
        }
    }

    /**
     * A constructor that creates a MailImpl with the specified name, sender,
     * recipients, and message data.
     *
     * @param name       the name of the MailImpl
     * @param sender     the sender for this MailImpl
     * @param recipients the collection of recipients of this MailImpl
     * @param messageIn  a stream containing the message source
     */
    public MailImpl(String name, MailAddress sender, Collection<MailAddress> recipients, InputStream messageIn)
            throws MessagingException {
        this(name, sender, recipients);
        MimeMessageSource source = new MimeMessageInputStreamSource(name, messageIn);
        // if MimeMessageCopyOnWriteProxy throws an error in the constructor we
        // have to manually care disposing our source.
        try {
            this.setMessage(new MimeMessageCopyOnWriteProxy(source));
        } catch (MessagingException e) {
            LifecycleUtil.dispose(source);
            throw e;
        }
    }

    /**
     * A constructor that creates a MailImpl with the specified name, sender,
     * recipients, and MimeMessage.
     */
    public MailImpl(String name, MailAddress sender, Collection<MailAddress> recipients, MimeMessage message)
            throws MessagingException {
        this(name, sender, recipients);
        this.setMessage(new MimeMessageCopyOnWriteProxy(message));
    }

    /**
     * Duplicate the MailImpl, replacing the mail name with the one passed in as
     * an argument.
     *
     * @param newName the name for the duplicated mail
     * @return a MailImpl that is a duplicate of this one with a different name
     */
    @VisibleForTesting
    Mail duplicate(String newName) {
        try {
            return new MailImpl(this, newName);
        } catch (MessagingException me) {
            // Ignored. Return null in the case of an error.
        }
        return null;
    }

    @Override
    public String getErrorMessage() {
        return errorMessage;
    }

    @Override
    public MimeMessage getMessage() throws MessagingException {
        return message;
    }

    @Override
    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    public Collection<MailAddress> getRecipients() {
        return recipients;
    }

    @Override
    public MailAddress getSender() {
        return sender;
    }

    @Override
    public String getState() {
        return state;
    }

    @Override
    public String getRemoteHost() {
        return remoteHost;
    }

    @Override
    public String getRemoteAddr() {
        return remoteAddr;
    }

    @Override
    public Date getLastUpdated() {
        return lastUpdated;
    }

    /**
     * <p>
     * Return the size of the message including its headers.
     * MimeMessage.getSize() method only returns the size of the message body.
     * </p>
     * <p/>
     * <p>
     * Note: this size is not guaranteed to be accurate - see Sun's
     * documentation of MimeMessage.getSize().
     * </p>
     *
     * @return approximate size of full message including headers.
     * @throws MessagingException if a problem occurs while computing the message size
     */
    @Override
    public long getMessageSize() throws MessagingException {
        return MimeMessageUtil.getMessageSize(message);
    }

    @Override
    public void setErrorMessage(String msg) {
        this.errorMessage = msg;
    }

    /**
     * Set the MimeMessage associated with this MailImpl.
     *
     * @param message the new MimeMessage associated with this MailImpl
     */
    @Override
    public void setMessage(MimeMessage message) throws MessagingException {

        // TODO: We should use the MimeMessageCopyOnWriteProxy
        // everytime we set the MimeMessage. We should
        // investigate if we should wrap it here

        if (this.message != message) {
            // If a setMessage is called on a Mail that already have a message
            // (discouraged) we have to make sure that the message we remove is
            // correctly unreferenced and disposed, otherwise it will keep locks
            if (this.message != null) {
                LifecycleUtil.dispose(this.message);
            }
            if (message instanceof MimeMessageCopyOnWriteProxy) {
                this.message = (MimeMessageCopyOnWriteProxy) message;
            } else {
                this.message = new MimeMessageCopyOnWriteProxy(message);
            }
        }
    }

    @Override
    public void setRecipients(Collection<MailAddress> recipients) {
        this.recipients = ImmutableList.copyOf(recipients);
    }

    public void setSender(MailAddress sender) {
        this.sender = sender;
    }

    @Override
    public void setState(String state) {
        this.state = state;
    }

    public void setRemoteHost(String remoteHost) {
        this.remoteHost = remoteHost;
    }

    public void setRemoteAddr(String remoteAddr) {
        this.remoteAddr = remoteAddr;
    }

    @Override
    public void setLastUpdated(Date lastUpdated) {
        // Make a defensive copy to ensure that the date
        // doesn't get changed external to the class
        if (lastUpdated != null) {
            lastUpdated = new Date(lastUpdated.getTime());
        }
        this.lastUpdated = lastUpdated;
    }

    /**
     * Writes the message out to an OutputStream.
     *
     * @param out the OutputStream to which to write the content
     * @throws MessagingException if the MimeMessage is not set for this MailImpl
     * @throws IOException        if an error occurs while reading or writing from the stream
     */
    public void writeMessageTo(OutputStream out) throws IOException, MessagingException {
        if (message != null) {
            message.writeTo(out);
        } else {
            throw new MessagingException("No message set for this MailImpl.");
        }
    }

    // Serializable Methods
    // TODO: These need some work. Currently very tightly coupled to
    // the internal representation.

    /**
     * Read the MailImpl from an <code>ObjectInputStream</code>.
     *
     * @param in the ObjectInputStream from which the object is read
     * @throws IOException            if an error occurs while reading from the stream
     * @throws ClassNotFoundException ?
     * @throws ClassCastException     if the serialized objects are not of the appropriate type
     */
    @SuppressWarnings("unchecked")
    private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
        try {
            Object obj = in.readObject();
            if (obj == null) {
                sender = null;
            } else if (obj instanceof String) {
                sender = new MailAddress((String) obj);
            } else if (obj instanceof MailAddress) {
                sender = (MailAddress) obj;
            }
        } catch (ParseException pe) {
            throw new IOException("Error parsing sender address: " + pe.getMessage());
        }
        recipients = (Collection<MailAddress>) in.readObject();
        state = (String) in.readObject();
        errorMessage = (String) in.readObject();
        name = (String) in.readObject();
        remoteHost = (String) in.readObject();
        remoteAddr = (String) in.readObject();
        setLastUpdated((Date) in.readObject());
        // the following is under try/catch to be backwards compatible
        // with messages created with James version <= 2.2.0a8
        try {
            setAttributesRaw((Map<String, Object>) in.readObject());
        } catch (OptionalDataException ode) {
            if (ode.eof) {
                attributes = new HashMap<>();
            } else {
                throw ode;
            }
        }
        perRecipientSpecificHeaders = (PerRecipientHeaders) in.readObject();
    }

    /**
     * Write the MailImpl to an <code>ObjectOutputStream</code>.
     *
     * @param out the ObjectOutputStream to which the object is written
     * @throws IOException if an error occurs while writing to the stream
     */
    private void writeObject(java.io.ObjectOutputStream out) throws IOException {
        out.writeObject(sender);
        out.writeObject(recipients);
        out.writeObject(state);
        out.writeObject(errorMessage);
        out.writeObject(name);
        out.writeObject(remoteHost);
        out.writeObject(remoteAddr);
        out.writeObject(lastUpdated);
        out.writeObject(getAttributesRaw());
        out.writeObject(perRecipientSpecificHeaders);
    }

    @Override
    public void dispose() {
        LifecycleUtil.dispose(message);
        message = null;
    }

    /**
     * <p>
     * This method is necessary, when Mail repositories needs to deal explicitly
     * with storing Mail attributes as a Serializable
     * </p>
     * <p>
     * <strong>Note</strong>: This method is not exposed in the Mail interface,
     * it is for internal use by James only.
     * </p>
     *
     * @return Serializable of the entire attributes collection
     * @since 2.2.0
     */
    public Map<String, Object> getAttributesRaw() {
        return attributes.values().stream().collect(Collectors.toMap(attribute -> attribute.getName().asString(),
                attribute -> attribute.getValue().value()));
    }

    /**
     * <p>
     * This method is necessary, when Mail repositories needs to deal explicitly
     * with retriving Mail attributes as a Serializable
     * </p>
     * <p>
     * <strong>Note</strong>: This method is not exposed in the Mail interface,
     * it is for internal use by James only.
     * </p>
     *
     * @param attr Serializable of the entire attributes collection
     * @since 2.2.0
     */
    public void setAttributesRaw(Map<String, Object> attr) {
        this.attributes = toAttributeMap(attr);
    }

    private void setAttributes(Map<AttributeName, Attribute> attr) {
        this.attributes = Maps.newHashMap(attr);
    }

    @Override
    public Stream<Attribute> attributes() {
        return this.attributes.values().stream();
    }

    @Override
    public Serializable getAttribute(String key) {
        return toSerializable(attributes.get(AttributeName.of(key)));
    }

    @Override
    public Optional<Attribute> getAttribute(AttributeName name) {
        return Optional.ofNullable(attributes.get(name));
    }

    @Override
    public Serializable setAttribute(String key, Serializable object) {
        Preconditions.checkNotNull(key, "Key of an attribute should not be null");
        Attribute attribute = Attribute.convertToAttribute(key, object);
        Attribute previous = attributes.put(attribute.getName(), attribute);

        return toSerializable(previous);
    }

    @Override
    public Optional<Attribute> setAttribute(Attribute attribute) {
        Preconditions.checkNotNull(attribute.getName().asString(), "AttributeName should not be null");
        return Optional.ofNullable(this.attributes.put(attribute.getName(), attribute));
    }

    @Override
    public Serializable removeAttribute(String key) {
        return toSerializable(attributes.remove(AttributeName.of(key)));
    }

    @Override
    public Optional<Attribute> removeAttribute(AttributeName attributeName) {
        Attribute previous = attributes.remove(attributeName);
        return Optional.ofNullable(previous);
    }

    @Override
    public void removeAllAttributes() {
        attributes.clear();
    }

    @Override
    public Iterator<String> getAttributeNames() {
        return attributes.keySet().stream().map(AttributeName::asString).iterator();
    }

    @Override
    public Stream<AttributeName> attributeNames() {
        return attributes().map(Attribute::getName);
    }

    private Serializable toSerializable(Attribute previous) {
        return (Serializable) Optional.ofNullable(previous).map(Attribute::getValue).map(AttributeValue::getValue)
                .orElse(null);
    }

    @Override
    public boolean hasAttributes() {
        return !attributes.isEmpty();
    }

    /**
     * This methods provide cloning for serializable objects. Mail Attributes
     * are Serializable but not Clonable so we need a deep copy
     *
     * @param o Object to be cloned
     * @return the cloned Object
     * @throws IOException
     * @throws ClassNotFoundException
     */
    private static Object cloneSerializableObject(Object o) throws IOException, ClassNotFoundException {
        ByteArrayOutputStream b = new ByteArrayOutputStream();
        try (ObjectOutputStream out = new ObjectOutputStream(b)) {
            out.writeObject(o);
            out.flush();
        }
        ByteArrayInputStream bi = new ByteArrayInputStream(b.toByteArray());
        try (ObjectInputStream in = new ObjectInputStream(bi)) {
            return in.readObject();
        }
    }

    /**
     * Generate a new identifier/name for a mail being processed by this server.
     *
     * @return the new identifier
     */
    public static String getId() {
        return "Mail" + System.currentTimeMillis() + "-" + UUID.randomUUID();
    }

    @Override
    public PerRecipientHeaders getPerRecipientSpecificHeaders() {
        return perRecipientSpecificHeaders;
    }

    @Override
    public void addSpecificHeaderForRecipient(Header header, MailAddress recipient) {
        perRecipientSpecificHeaders.addHeaderForRecipient(header, recipient);
    }

    public void addAllSpecificHeaderForRecipient(PerRecipientHeaders perRecipientHeaders) {
        perRecipientSpecificHeaders.addAll(perRecipientHeaders);
    }

    @Override
    public Mail duplicate() throws MessagingException {
        return MailImpl.duplicate(this);
    }
}