blob: 691cda712c132b218763ef956eeb6f33b6bfb91b [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.transport.mailets.managesieve;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;
import jakarta.inject.Inject;
import jakarta.mail.MessagingException;
import org.apache.james.core.Username;
import org.apache.james.managesieve.api.Session;
import org.apache.james.managesieve.api.SieveParser;
import org.apache.james.managesieve.core.CoreProcessor;
import org.apache.james.managesieve.transcode.ArgumentParser;
import org.apache.james.managesieve.transcode.ManageSieveProcessor;
import org.apache.james.managesieve.util.SettableSession;
import org.apache.james.sieverepository.api.SieveRepository;
import org.apache.james.transport.mailets.managesieve.transcode.MessageToCoreToMessage;
import org.apache.james.user.api.UsersRepository;
import org.apache.mailet.Experimental;
import org.apache.mailet.Mail;
import org.apache.mailet.base.GenericMailet;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.annotations.VisibleForTesting;
/**
* <code>ManageSieveMailet</code> interprets mail from a local sender as
* commands to manage Sieve scripts stored on the mail server. The commands are
* a subset of those defined by <a
* href=http://tools.ietf.org/html/rfc5804#section
* -2>http://tools.ietf.org/html/rfc5804#section-2 "MessageToCoreToMessage"</a>.
*
* <p>
* For each supported command and associated response, the format is the same as
* defined by RFC 5804, with the exception that when Sieve scripts are involved
* in the exchange they are attached to the mail with the MIME type of
* 'application/sieve' rather than being embedded in the command.
*
* <p>
* The command is written in the subject header of a mail received by this
* mailet. Responses from this mailet are sent to the sender as mail with the
* message body containing the response.
*
* <p>
* The following commands are supported:
* <ul>
* <li><a href=http://tools.ietf.org/html/rfc5804#section-2.4>CAPABILITY</a>
* <li><a href=http://tools.ietf.org/html/rfc5804#section-2.5>HAVESPACE</a>
* <li><a href=http://tools.ietf.org/html/rfc5804#section-2.6>PUTSCRIPT</a>
* <li><a href=http://tools.ietf.org/html/rfc5804#section-2.7>LISTSCRIPTS</a>
* <li><a href=http://tools.ietf.org/html/rfc5804#section-2.8>SETACTIVE</a>
* <li><a href=http://tools.ietf.org/html/rfc5804#section-2.9>GETSCRIPT</a>
* <li><a href=http://tools.ietf.org/html/rfc5804#section-2.10>DELETESCRIPT</a>
* <li><a href=http://tools.ietf.org/html/rfc5804#section-2.11>RENAMESCRIPT</a>
* <li><a href=http://tools.ietf.org/html/rfc5804#section-2.12>CHECKSCRIPT</a>
* </ul>
*
* <h2>An Important Note About Security</h2>
* <p>
* The mail server on which this mailet is deployed MUST robustly authenticate
* the sender, who MUST be local.
* <p>
* Sieve provides powerful email processing capabilities that if hijacked can
* expose the mail of individuals and organisations to intruders.
*/
@Experimental
public class ManageSieveMailet extends GenericMailet implements MessageToCoreToMessage.HelpProvider {
private static final Logger LOGGER = LoggerFactory.getLogger(ManageSieveMailet.class);
// Injected
private SieveRepository sieveRepository = null;
// Injected
private SieveParser sieveParser = null;
private UsersRepository usersRepository;
private MessageToCoreToMessage transcoder = null;
private URL helpURL = null;
private String help = null;
private boolean cache = true;
@Override
public void init() throws MessagingException {
super.init();
// Validate resources
if (null == sieveParser) {
throw new MessagingException("Missing resource \"sieveparser\"");
}
if (null == sieveRepository) {
throw new MessagingException("Missing resource \"sieverepository\"");
}
setHelpURL(getInitParameter("helpURL"));
cache = getInitParameter("cache", true);
transcoder = new MessageToCoreToMessage(new ManageSieveProcessor(
new ArgumentParser(new CoreProcessor(sieveRepository, usersRepository, sieveParser), false)),
this);
}
@Override
public void service(Mail mail) throws MessagingException {
// Sanity checks
if (!mail.hasSender()) {
LOGGER.error("Sender is null");
return;
}
if (!getMailetContext().isLocalServer(mail.getMaybeSender().get().getDomain())) {
LOGGER.error("Sender not local");
return;
}
// Update the Session for the current mail and execute
SettableSession session = new SettableSession();
if (mail.getAttribute(Mail.SMTP_AUTH_USER).isPresent()) {
session.setState(Session.State.AUTHENTICATED);
} else {
session.setState(Session.State.UNAUTHENTICATED);
}
session.setUser(Username.of(mail.getMaybeSender().get().asString()));
getMailetContext().sendMail(
mail.getRecipients().iterator().next(),
mail.getMaybeSender().asList(),
transcoder.execute(session, mail.getMessage()));
mail.setState(Mail.GHOST);
// And tidy up
clearCaches();
}
@Inject
public void setSieveRepository(SieveRepository repository) {
sieveRepository = repository;
}
@Inject
public void setSieveParser(SieveParser sieveParser) {
this.sieveParser = sieveParser;
}
@Inject
public void setUsersRepository(UsersRepository usersRepository) {
this.usersRepository = usersRepository;
}
@Override
public String getMailetInfo() {
return getClass().getName();
}
private void setHelpURL(String helpURL) throws MessagingException {
try {
this.helpURL = new URI(helpURL).toURL();
} catch (MalformedURLException | URISyntaxException ex) {
throw new MessagingException("Invalid helpURL", ex);
}
}
private void clearCaches() {
if (!cache) {
help = null;
}
}
@Override
@VisibleForTesting
public String getHelp() throws MessagingException {
if (null == help) {
help = computeHelp();
}
return help;
}
private String computeHelp() throws MessagingException {
try (InputStream stream = helpURL.openStream();
Scanner scanner = new Scanner(stream, StandardCharsets.UTF_8)) {
return scanner.useDelimiter("\\A").next();
} catch (IOException ex) {
throw new MessagingException("Unable to access help URL: " + helpURL.toExternalForm(), ex);
}
}
}