package org.subethamail.core.smtp; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.List; import java.util.logging.Level; import java.util.logging.LogRecord; import lombok.extern.java.Log; import org.subethamail.smtp.MessageContext; import org.subethamail.smtp.MessageHandler; import org.subethamail.smtp.MessageHandlerFactory; import org.subethamail.smtp.RejectException; import org.subethamail.smtp.TooMuchDataException; import org.subethamail.smtp.client.SMTPException; import org.subethamail.smtp.client.SmartClient; import org.subethamail.smtp.io.DeferredFileOutputStream; /** * This is the master SMTP handler for SubEtha. * * Mail may arrive with one or more recipients. Every recipient accepted by * the Injector results in a separate, split injection. * * There may be an SMTP default host which is offered any addresses that are not * accepted by SubEtha. The host is delivered only a single SMTP transaction for * all non-mailinglist addresses. * * Some example cases: * * 1) Message arrives for single recipient, a valid list address. It is handed to * the Injector as a raw data stream on the socket. * 2) Message arrives for multiple recipients, all valid lists. It is split and * injected once for each list. * 3) Message arrives for one or more defaulted addresses. The addresses are * checked via SMTP RCPT TO on the default host and then a single DATA is * written to the server. * 4) Message arrives for two lists and two defaulted addresses (both accepted via * RCPT TO). The data is split three times, injected twice into SubEtha and * then sent once to the (already RCPT TO'd) default host. * * @author Jeff Schnitzer */ @Log public class SMTPHandler implements MessageHandlerFactory { /** Beyond this, we buffer to disk instead of memory. 10MB */ static final int DATA_DEFERRED_SIZE = 1024 * 1024 * 10; /** */ protected SMTPService smtpService; /** */ public SMTPHandler(SMTPService service) { this.smtpService = service; } /* */ public MessageHandler create(MessageContext ctx) { return new Handler(ctx); } /** * The actual handler implementation */ public class Handler implements MessageHandler { MessageContext ctx; String from; List<String> ourLists = new ArrayList<String>(); SmartClient fallbackConnection; /** */ Handler(MessageContext ctx) { this.ctx = ctx; } /** */ public void from(String from) throws RejectException { this.from = from; } /** */ public void recipient(String recipient) throws RejectException { if (SMTPHandler.this.smtpService.getInjector().accept(recipient)) { this.ourLists.add(recipient); } else { SmartClient client = this.getFallbackConnection(); if (client == null) { // No defaulting config, reject it throw new RejectException(553, "<" + recipient + "> address unknown"); } else { try { try { if (!client.sentFrom()) client.from(this.from); client.to(recipient); } catch (SMTPException ex) { throw new RejectException(ex.getResponse().getCode(), ex.getResponse().getMessage()); } } catch (IOException ex) { throw new RejectException(554, ex.getMessage()); } } } } /** */ public void data(InputStream data) throws RejectException, TooMuchDataException, IOException { List<Deliverer> deliveries = this.makeDeliverers(); if (deliveries.size() == 1) { deliveries.get(0).deliver(data); } else { DeferredFileOutputStream dfos = new DeferredFileOutputStream(DATA_DEFERRED_SIZE); try { // Buffer it int value; while ((value = data.read()) >= 0) dfos.write(value); // Deliver it, but track the last exception just in case // nobody succeeds - we'll throw it as if it was critical. boolean anyoneAtAll = false; Exception lastProblem = null; for (Deliverer deliv: deliveries) { try { deliv.deliver(dfos.getInputStream()); anyoneAtAll = true; } catch (Exception ex) { LogRecord logRecord=new LogRecord(Level.SEVERE, "Error delivering to {0}"); logRecord.setThrown(ex);; logRecord.setParameters(new Object[]{deliv.toString()}); log.log(logRecord); lastProblem = ex; } } if (!anyoneAtAll) { if (lastProblem instanceof IOException) throw (IOException)lastProblem; else if (lastProblem instanceof RejectException) throw (RejectException)lastProblem; else if (lastProblem instanceof RuntimeException) throw (RuntimeException)lastProblem; else throw new RuntimeException(lastProblem); } } finally { dfos.close(); } } } /** */ public void done() { if (this.fallbackConnection != null) this.fallbackConnection.close(); } /** * Gets the current connection to the remote host, or creates one. * @throws RejectException if something goes wrong connecting to the backend */ SmartClient getFallbackConnection() throws RejectException { if (this.fallbackConnection == null) { String hostAndPort = SMTPHandler.this.smtpService.getFallbackHost(); if (hostAndPort != null) { String[] split = hostAndPort.split(":"); int port = split.length > 1 ? Integer.parseInt(split[1]) : 25; try { this.fallbackConnection = new SmartClient(split[0], port, this.ctx.getSMTPServer().getHostName()); } catch (IOException e) { throw new RejectException(554, e.toString()); } } } return this.fallbackConnection; } /** Makes a nice list of deliverers based on who wants our data */ List<Deliverer> makeDeliverers() { List<Deliverer> result = new ArrayList<Deliverer>(this.ourLists.size() + 1); for (String list: this.ourLists) result.add(new OurDeliverer(SMTPHandler.this.smtpService.getInjector(), this.from, list)); if ((this.fallbackConnection != null) && this.fallbackConnection.sentTo()) result.add(new FallbackDeliverer(this.fallbackConnection)); return result; } } }