blob: bcf929d0c1d56d83e7bdcde3375c514f645730a3 [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.mailetcontainer.lib;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentLinkedDeque;
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.inject.Inject;
import jakarta.mail.MessagingException;
import org.apache.commons.configuration2.HierarchicalConfiguration;
import org.apache.commons.configuration2.ex.ConfigurationException;
import org.apache.commons.configuration2.tree.ImmutableNode;
import org.apache.james.core.MailAddress;
import org.apache.james.lifecycle.api.Configurable;
import org.apache.james.mailetcontainer.api.MailProcessor;
import org.apache.james.mailetcontainer.api.MailetLoader;
import org.apache.james.mailetcontainer.api.MatcherLoader;
import org.apache.james.mailetcontainer.impl.MailetConfigImpl;
import org.apache.james.mailetcontainer.impl.MatcherConfigImpl;
import org.apache.james.mailetcontainer.impl.MatcherMailetPair;
import org.apache.james.mailetcontainer.impl.ProcessorImpl;
import org.apache.james.mailetcontainer.impl.jmx.JMXStateMailetProcessorListener;
import org.apache.james.mailetcontainer.impl.matchers.CompositeMatcher;
import org.apache.mailet.Mail;
import org.apache.mailet.Mailet;
import org.apache.mailet.MailetConfig;
import org.apache.mailet.MailetContext;
import org.apache.mailet.Matcher;
import org.apache.mailet.MatcherConfig;
import org.apache.mailet.base.MatcherInverter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.collect.ImmutableList;
/**
* Abstract base class for {@link MailProcessor} implementations which want to
* process {@link Mail} via {@link Matcher} and {@link Mailet}
*/
public abstract class AbstractStateMailetProcessor implements MailProcessor, Configurable {
private static final Logger LOGGER = LoggerFactory.getLogger(AbstractStateMailetProcessor.class);
private MailetContext mailetContext;
private MatcherLoader matcherLoader;
private MailProcessor rootMailProcessor;
private final Collection<MailetProcessorListener> listeners = new ConcurrentLinkedDeque<>();
private JMXStateMailetProcessorListener jmxListener;
private boolean enableJmx = true;
private HierarchicalConfiguration<ImmutableNode> config;
private MailetLoader mailetLoader;
private final List<MatcherMailetPair> pairs = new ArrayList<>();
private String state;
public void setMatcherLoader(MatcherLoader matcherLoader) {
this.matcherLoader = matcherLoader;
}
public void setRootMailProcessor(MailProcessor rootMailProcessor) {
this.rootMailProcessor = rootMailProcessor;
}
@Inject
public void setMailetContext(MailetContext mailetContext) {
this.mailetContext = mailetContext;
}
@Inject
public void setMailetLoader(MailetLoader mailetLoader) {
this.mailetLoader = mailetLoader;
}
@Override
public void configure(HierarchicalConfiguration<ImmutableNode> config) throws ConfigurationException {
this.state = config.getString("[@state]", null);
if (state == null) {
throw new ConfigurationException("Processor state attribute must be configured");
}
if (state.equals(Mail.GHOST)) {
throw new ConfigurationException("Processor state of " + Mail.GHOST + " is reserved for internal use, choose a different one");
}
this.enableJmx = config.getBoolean("[@enableJmx]", true);
this.config = config;
}
/**
* Init the container
*/
@PostConstruct
public void init() throws Exception {
parseConfiguration();
setupRouting(pairs);
if (enableJmx) {
this.jmxListener = new JMXStateMailetProcessorListener(state, this);
addListener(jmxListener);
}
}
/**
* Destroy the container
*/
@PreDestroy
public void destroy() {
listeners.clear();
if (enableJmx && jmxListener != null) {
jmxListener.dispose();
}
for (MatcherMailetPair pair : pairs) {
Mailet mailet = pair.getMailet();
Matcher matcher = pair.getMatcher();
LOGGER.debug("Shutdown matcher {}", matcher.getMatcherInfo());
matcher.destroy();
LOGGER.debug("Shutdown mailet {}", mailet.getMailetInfo());
mailet.destroy();
}
}
/**
* Hand the mail over to another processor
*/
protected void toProcessor(Mail mail) throws MessagingException {
rootMailProcessor.service(mail);
}
protected String getState() {
return state;
}
/**
* Return a unmodifiable {@link List} of the configured {@link Mailet}'s
*/
public List<Mailet> getMailets() {
return pairs.stream()
.map(MatcherMailetPair::getMailet)
.collect(ImmutableList.toImmutableList());
}
/**
* Return a unmodifiable {@link List} of the configured {@link Matcher}'s
*/
public List<Matcher> getMatchers() {
return pairs.stream()
.map(MatcherMailetPair::getMatcher)
.collect(ImmutableList.toImmutableList());
}
public void addListener(MailetProcessorListener listener) {
listeners.add(listener);
}
public List<MailetProcessorListener> getListeners() {
return ImmutableList.copyOf(listeners);
}
/**
* Create a {@link MailetConfig} for the given mailetname and configuration
*/
private MailetConfig createMailetConfig(String mailetName, HierarchicalConfiguration<ImmutableNode> configuration) {
final MailetConfigImpl configImpl = new MailetConfigImpl();
configImpl.setMailetName(mailetName);
configImpl.setConfiguration(configuration);
configImpl.setMailetContext(mailetContext);
return configImpl;
}
/**
* Create a {@link MatcherConfig} for the given "match=" attribute.
*/
private MatcherConfig createMatcherConfig(String matchName) {
String condition = null;
int i = matchName.indexOf('=');
if (i != -1) {
condition = matchName.substring(i + 1);
matchName = matchName.substring(0, i);
}
final MatcherConfigImpl configImpl = new MatcherConfigImpl();
configImpl.setMatcherName(matchName);
configImpl.setCondition(condition);
configImpl.setMailetContext(mailetContext);
return configImpl;
}
/**
* Load {@link CompositeMatcher} implementations and their child
* {@link Matcher}'s
*
* CompositeMatcher were added by JAMES-948
*
* @return compositeMatchers
*/
private List<Matcher> loadCompositeMatchers(String state, Map<String, Matcher> compMap, List<HierarchicalConfiguration<ImmutableNode>> compMatcherConfs) throws ConfigurationException, MessagingException {
List<Matcher> matchers = new ArrayList<>();
for (HierarchicalConfiguration<ImmutableNode> c : compMatcherConfs) {
String compName = c.getString("[@name]", null);
String matcherName = c.getString("[@match]", null);
String invertedMatcherName = c.getString("[@notmatch]", null);
Matcher matcher = null;
if (matcherName != null && invertedMatcherName != null) {
// if no matcher is configured throw an Exception
throw new ConfigurationException("Please configure only match or nomatch per mailet");
} else if (matcherName != null) {
matcher = matcherLoader.getMatcher(createMatcherConfig(matcherName));
if (matcher instanceof CompositeMatcher) {
CompositeMatcher compMatcher = (CompositeMatcher) matcher;
List<Matcher> childMatcher = loadCompositeMatchers(state, compMap, c.configurationsAt("matcher"));
for (Matcher aChildMatcher : childMatcher) {
compMatcher.add(aChildMatcher);
}
}
} else if (invertedMatcherName != null) {
Matcher m = matcherLoader.getMatcher(createMatcherConfig(invertedMatcherName));
if (m instanceof CompositeMatcher) {
CompositeMatcher compMatcher = (CompositeMatcher) m;
List<Matcher> childMatcher = loadCompositeMatchers(state, compMap, c.configurationsAt("matcher"));
for (Matcher aChildMatcher : childMatcher) {
compMatcher.add(aChildMatcher);
}
}
matcher = new MatcherInverter(m);
}
if (matcher == null) {
throw new ConfigurationException("Unable to load matcher instance");
}
matchers.add(matcher);
if (compName != null) {
// check if there is already a composite Matcher with the name
// registered in the processor
if (compMap.containsKey(compName)) {
throw new ConfigurationException("CompositeMatcher with name " + compName + " is already defined in processor " + state);
}
compMap.put(compName, matcher);
}
}
return matchers;
}
private void parseConfiguration() throws MessagingException, ConfigurationException {
// load composite matchers if there are any
Map<String, Matcher> compositeMatchers = new HashMap<>();
loadCompositeMatchers(getState(), compositeMatchers, config.configurationsAt("matcher"));
final List<HierarchicalConfiguration<ImmutableNode>> mailetConfs = config.configurationsAt("mailet");
// Loop through the mailet configuration, load
// all of the matcher and mailets, and add
// them to the processor.
for (HierarchicalConfiguration<ImmutableNode> c : mailetConfs) {
// We need to set this because of correctly parsing comma
String mailetClassName = c.getString("[@class]");
String matcherName = c.getString("[@match]", null);
String invertedMatcherName = c.getString("[@notmatch]", null);
Mailet mailet;
Matcher matcher;
try {
if (matcherName != null && invertedMatcherName != null) {
// if no matcher is configured throw an Exception
throw new ConfigurationException("Please configure only match or nomatch per mailet");
} else if (matcherName != null) {
// try to load from compositeMatchers first
matcher = compositeMatchers.get(matcherName);
if (matcher == null) {
// no composite Matcher found, try to load it via
// MatcherLoader
matcher = matcherLoader.getMatcher(createMatcherConfig(matcherName));
}
} else if (invertedMatcherName != null) {
// no composite Matcher found, try to load it via MatcherLoader
matcher = matcherLoader.getMatcher(createMatcherConfig(invertedMatcherName));
matcher = new MatcherInverter(matcher);
} else {
// default matcher is All
matcher = matcherLoader.getMatcher(createMatcherConfig("All"));
LOGGER.debug("Mailet {} has no 'match' attribute. Defaulting to match all mails.", mailetClassName);
}
// The matcher itself should log that it's been inited.
LOGGER.info("Matcher {} instantiated.", matcherName);
} catch (MessagingException ex) {
// **** Do better job printing out exception
LOGGER.error("Unable to init matcher {}", matcherName, ex);
if (ex.getNextException() != null) {
LOGGER.error("Caused by nested exception: ", ex.getNextException());
}
throw new ConfigurationException("Unable to init matcher " + matcherName, ex);
}
try {
mailet = mailetLoader.getMailet(createMailetConfig(mailetClassName, c));
LOGGER.info("Mailet {} instantiated.", mailetClassName);
} catch (MessagingException ex) {
// **** Do better job printing out exception
LOGGER.error("Unable to init mailet {}", mailetClassName, ex);
if (ex.getNextException() != null) {
LOGGER.error("Caused by nested exception: ", ex.getNextException());
}
throw new ConfigurationException("Unable to init mailet " + mailetClassName, ex);
}
if (matcher != null && mailet != null) {
pairs.add(new MatcherMailetPair(matcher, mailet));
} else {
throw new ConfigurationException("Unable to load Mailet or Matcher");
}
}
}
/**
* Setup the routing for the configured {@link MatcherMailetPair}'s for this
* {@link ProcessorImpl}
*/
protected abstract void setupRouting(List<MatcherMailetPair> pairs) throws MessagingException;
/**
* A Listener which will get notified after
* {@link Mailet#service(org.apache.mailet.Mail)} and
* {@link Matcher#match(org.apache.mailet.Mail)} methods are called from the
* container
*/
public interface MailetProcessorListener {
/**
* Get called after each {@link Mailet} call was complete
*
* @param processTime
* in ms
* @param e
* or null if no Exception was thrown
*/
void afterMailet(Mailet m, String mailName, String state, long processTime, Throwable e);
/**
* Get called after each {@link Matcher} call was complete
*
* @param processTime
* in ms
* @param e
* or null if no Exception was thrown
*/
void afterMatcher(Matcher m, String mailName, Collection<MailAddress> recipients, Collection<MailAddress> matches, long processTime, Throwable e);
}
}