blob: d1c5aa74c82d7488257677d5cf420ec7cf92b215 [file] [log] [blame]
/****************************************************************
* 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.IOException;
import java.io.ObjectInputStream;
import java.io.OptionalDataException;
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 jakarta.mail.Address;
import jakarta.mail.MessagingException;
import jakarta.mail.internet.AddressException;
import jakarta.mail.internet.InternetAddress;
import jakarta.mail.internet.MimeMessage;
import jakarta.mail.internet.ParseException;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
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 duplicateWithoutMessage(mail)
.mimeMessage(mail.getMessage())
.build();
}
public static MailImpl.Builder duplicateWithoutMessage(Mail mail) throws MessagingException {
return MailImpl.builder()
.name(deriveNewName(mail.getName()))
.sender(mail.getMaybeSender())
.addRecipients(mail.getRecipients())
.remoteHost(mail.getRemoteHost())
.remoteAddr(mail.getRemoteAddr())
.lastUpdated(mail.getLastUpdated())
.errorMessage(mail.getErrorMessage())
.addAttributes(duplicateAttributes(mail))
.addAllHeadersForRecipients(mail.getPerRecipientSpecificHeaders());
}
private static ImmutableList<Attribute> duplicateAttributes(Mail mail) {
try {
return mail.attributes().map(Attribute::duplicate).collect(ImmutableList.toImmutableList());
} catch (IllegalStateException e) {
LOGGER.error("Error while cloning Mail attributes", e);
return ImmutableList.of();
}
}
public static MailImpl fromMimeMessage(String name, MimeMessage mimeMessage) throws MessagingException {
return MailImpl.builder()
.name(name)
.sender(getSender(mimeMessage))
.addRecipients(getRecipients(mimeMessage))
.mimeMessage(mimeMessage)
.build();
}
public static RequireName builder() {
return Builder::new;
}
public interface RequireName {
Builder name(String name);
}
public static class Builder {
private final String name;
private Optional<MimeMessage> mimeMessage;
private List<MailAddress> recipients;
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(String name) {
Preconditions.checkNotNull(name);
Preconditions.checkArgument(!name.isEmpty(), "name must not be empty");
this.name = name;
mimeMessage = Optional.empty();
recipients = Lists.newArrayList();
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 String getName() {
return name;
}
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 addRecipients(Collection<MailAddress> recipients) {
this.recipients.addAll(recipients);
return this;
}
public Builder addRecipients(MailAddress... recipients) {
return addRecipients(ImmutableList.copyOf(recipients));
}
public Builder addRecipients(String... recipients) {
return addRecipients(Arrays.stream(recipients)
.map(Throwing.function(MailAddress::new))
.collect(ImmutableList.toImmutableList()));
}
public Builder addRecipient(MailAddress recipient) {
return addRecipients(recipient);
}
public Builder addRecipient(String recipient) throws AddressException {
return addRecipients(recipient);
}
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 addAttribute(String name, Serializable object) {
return addAttribute(Attribute.convertToAttribute(name, object));
}
public Builder addAttribute(Attribute attribute) {
this.attributes.put(attribute.getName(), attribute);
return this;
}
public Builder addAttributes(Collection<Attribute> attributes) {
attributes.forEach(this::addAttribute);
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(name, state.orElse(DEFAULT), attributes, recipients, perRecipientHeaders);
mimeMessage.ifPresent(Throwing.consumer(mail::setMessage).sneakyThrow());
sender.ifPresent(mail::setSender);
errorMessage.ifPresent(mail::setErrorMessage);
lastUpdated.ifPresent(mail::setLastUpdated);
remoteAddr.ifPresent(mail::setRemoteAddr);
remoteHost.ifPresent(mail::setRemoteHost);
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 = 5;
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.randomAlphanumeric(suffixLength - 1);
}
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 MimeMessageWrapper 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;
private MailImpl(String name,
String state,
Map<AttributeName, Attribute> attributes,
List<MailAddress> recipients,
PerRecipientHeaders perRecipientHeaders) {
setName(name);
setState(state);
setAttributes(attributes);
setRecipients(recipients);
perRecipientSpecificHeaders = perRecipientHeaders;
}
@Override
public String getErrorMessage() {
return errorMessage;
}
@Override
public MimeMessage getMessage() throws MessagingException {
return message;
}
@Override
public void setName(String name) {
Preconditions.checkNotNull(name);
Preconditions.checkArgument(!name.isEmpty(), "name must not be empty");
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 {
setMessageNoCopy(new MimeMessageWrapper(message));
}
public void setMessageNoCopy(MimeMessageWrapper message) throws MessagingException {
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);
}
this.message = message;
}
}
public void setMessageContent(MimeMessageSource message) throws MessagingException {
setMessageNoCopy(new MimeMessageWrapper(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;
}
// 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());
try {
setAttributesUsingJsonable(in);
} catch (Exception e) {
setAttributesUsingJavaSerializable(in);
}
perRecipientSpecificHeaders = (PerRecipientHeaders) in.readObject();
}
/**
* Newest mailet API introduced {@link AttributeValue} which can encapsulate any class, possible not serializable.
*
* As such, algorithm relying on out of the box serialization can not handle non serializable attribute values as well as
* nested AttributeValue.
*
* Thus, rather than Java deserializing attributes we deserialize them as Json using AttributeValue capabilities.
*/
private void setAttributesUsingJsonable(ObjectInputStream in) throws IOException, ClassNotFoundException {
@SuppressWarnings("unchecked")
Map<String, String> attributesAsJson = (Map<String, String>) in.readObject();
this.attributes = attributesAsJson.entrySet().stream()
.map(Throwing.function(entry -> new Attribute(AttributeName.of(entry.getKey()), AttributeValue.fromJsonString(entry.getValue()))))
.collect(Collectors.toMap(
Attribute::getName,
Function.identity()));
}
/**
* Fallback to Java deserialization if {@link MailImpl#setAttributesUsingJsonable(ObjectInputStream)} fails.
*/
@SuppressWarnings("unchecked")
private void setAttributesUsingJavaSerializable(ObjectInputStream in) throws IOException, ClassNotFoundException {
// 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;
}
}
}
/**
* 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(getAttributesAsJson());
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()));
}
/**
* Newly serialized emails are serialized using {@link AttributeValue}.
*
* Upon deserialization, fallback to Java deserialization is handled to not introduce retro-compatibility issues.
*/
private Map<String, String> getAttributesAsJson() {
return attributes.values()
.stream()
.flatMap(entry -> entry.getValue().toJson().map(value -> Pair.of(entry.getName().asString(), value.toString())).stream())
.collect(ImmutableMap.toImmutableMap(Pair::getKey, Pair::getValue));
}
/**
* <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();
}
/**
* 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() + "-" + RandomStringUtils.randomAlphanumeric(20);
}
@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);
}
@Override
public Map<AttributeName, Attribute> attributesMap() {
return ImmutableMap.copyOf(attributes);
}
}