package votorola.a.mail; // Copyright 2007-2008, Michael Allan. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Votorola Software"), to deal in the Votorola Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicence, and/or sell copies of the Votorola Software, and to permit persons to whom the Votorola Software is furnished to do so, subject to the following conditions: The preceding copyright notice and this permission notice shall be included in all copies or substantial portions of the Votorola Software. THE VOTOROLA SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE VOTOROLA SOFTWARE OR THE USE OR OTHER DEALINGS IN THE VOTOROLA SOFTWARE. import java.io.*; import java.util.*; import java.util.concurrent.*; import java.util.concurrent.atomic.*; import java.util.logging.*; import votorola.g.logging.*; import java.util.regex.*; import java.sql.SQLException; import javax.activation.*; import javax.mail.*; import javax.mail.internet.*; import javax.script.*; import org.omg.CORBA.FloatHolder; import votorola._.*; import votorola.a.*; import votorola.g.hold.*; import votorola.g.lang.*; import votorola.a.locale.*; import votorola.g.mail.*; import votorola.g.script.*; /** The mail responder daemon. * The single instance of MailResponder is available via MailResponder.i(). */ public final @ThreadSafe class MailResponder implements Runnable { /** The single instance of MailResponder. */ static MailResponder i() { return instanceA.get(); } private static final AtomicReference instanceA = new AtomicReference(); /** Creates the single instance of MailResponder, * and makes it available via {@linkplain #i() i}(). * * @throws AddressException if misconfigured * @throws ScriptException if misconfigured * @throws java.sql.SQLException if unable to establish initial database connections */ MailResponder() throws AddressException, IOException, ScriptException, SQLException { if( !instanceA.compareAndSet( /*expect*/null, MailResponder.this )) throw new IllegalStateException(); // Class.forName( "org.postgresql.Driver" ); // register the database driver // Connection databaseConnection = DriverManager.getConnection( url, username, password ); metaService = MailMetaService.newMetaService( subserverRun, cc.s ); subserverRun.init_putElectoralService( metaService ); thread.start(); VOMailRD.i().spool().add( new Hold() { public @ThreadSafe void release() { thread.tryJoin( 4000/*ms*/ ); } // wait for it to stop gracefully (about as long as a user would wait) }); cc = null; // done with it, free the memory } // ```````````````````````````````````````````````````````````````````````````````````` // Initialized early, for use in other initializers. private final ElectoralSubserver.Run subserverRun = new ElectoralSubserver( System.getProperty( "user.name" )) .new Run( /*isSingleThreaded*/true ); ConfigurationContext cc = ConfigurationContext.configure( // nulled after init subserverRun.subserver(), compileConfigurationScript( subserverRun.subserver() )); // ------------------------------------------------------------------------------------ /** Executes the configuration script of the mail responder * (without making any configuration calls), thus compiling it for subsequent use. */ public static JavaScriptIncluder compileConfigurationScript( final ElectoralSubserver subserver ) throws ScriptException { return new JavaScriptIncluder( new File( subserver.votorolaDirectory(), "vomailrd.js" )); } /** The configuration file for this responder daemon. It is located at: *
* {@linkplain ElectoralSubserver#votorolaDirectory() votorolaDirectory}/vomailrd.js *
*

* The language is JavaScript. There are restrictions * on the {@linkplain votorola.g.script.JavaScriptIncluder character encoding}. *

*/ File configurationFile() { return configurationFile; } private final File configurationFile = cc.configurationFile; /** The protocol and location of the inbox. * Supported protocols are IMAP, Maildir and POP3. For example: *
      *   "imap:?" (actually, we're unsure of IMAP syntax, and have not tested it), per:
      *     http://java.sun.com/products/javamail/javadocs/com/sun/mail/imap/package-summary.html
      *
      *   "maildir:/home/subserverName/.mail"
      *     http://javamaildir.sourceforge.net/
      *
      *   "pop3://subserverName:password@host:port" (not yet tested), per:
      *     http://java.sun.com/products/javamail/javadocs/com/sun/mail/pop3/package-summary.html
      *   
* * @see ConfigurationContext#setInboxStoreURLName(String) */ URLName inboxStoreURLName() { return inboxStoreURLName; } private final URLName inboxStoreURLName = new URLName( cc.getInboxStoreURLName() ); /** Returns true if this daemon is to run without making any persistent state changes. * It will run as usual, in that case, but without actually writing to any database; * nor replying to any incoming message; nor altering any other significant state * that would persist and affect the next run. *

* Consequently, each dry run will read the messages of the inbox * over and over again, in an endless loop. *

* * @see ConfigurationContext#setDryRun(boolean) */ boolean isDryRun() { return dryRun; } private final boolean dryRun = cc.isDryRun(); // ==================================================================================== /** A context for configuring the mail responder daemon. * The daemon is configured by the responder's * {@linkplain #configurationFile configuration file}, * which contains a script (s) for that purpose. * During construction of the daemon, an instance of this context (daemonCC) * is passed to s, via s::configureMailRD(daemonCC). */ public static @ThreadSafe final class ConfigurationContext // public class and getters, accessible by configuration scripts { /** Constructs the complete configuration of the responder daemon. * * @param s the compiled configuration script */ static ConfigurationContext configure( ElectoralSubserver subserver, JavaScriptIncluder s ) throws ScriptException { final ConfigurationContext cc = new ConfigurationContext( subserver, s ); s.invokeKnownFunction( "configureMailRD", cc ); return cc; } private ConfigurationContext( ElectoralSubserver subserver, JavaScriptIncluder s ) { this.s = s; configurationFile = s.scriptFile(); inboxStoreURLName = "maildir:/home/" + subserver.name() + "/Maildir"; transferService = new SMTPTransportX.ConfigurationContext( configurationFile ); } private final File configurationFile; private final JavaScriptIncluder s; // -------------------------------------------------------------------------------- /** Returns the delay prior to each poll of the inbox for new messages. * If the inbox gets busy, then this value is ignored * and messages are read without delay. * * @see #setInboxPollSleepSeconds(int) */ public int getInboxPollSleepSeconds() { return inboxPollSleepSeconds; } private int inboxPollSleepSeconds = 20; /** Sets the delay prior to each poll of the inbox. * The default value is 20 seconds. * * @see #getInboxPollSleepSeconds() */ @ThreadRestricted("constructor") public void setInboxPollSleepSeconds( int inboxPollSleepSeconds ) { this.inboxPollSleepSeconds = inboxPollSleepSeconds; } /** @see MailResponder#inboxStoreURLName() * @see #setInboxStoreURLName(String) */ public String getInboxStoreURLName() { return inboxStoreURLName; } private String inboxStoreURLName; /** Sets the protocol and location of the inbox. The default value is * "maildir:/home/{@linkplain ElectoralSubserver#name() subserver-name}/Maildir". * * @see MailResponder#inboxStoreURLName() */ @ThreadRestricted("constructor") public void setInboxStoreURLName( String inboxStoreURLName ) { this.inboxStoreURLName = inboxStoreURLName; } /** @see MailResponder#isDryRun() * @see #setDryRun(boolean) */ public final boolean isDryRun() { return dryRun; }; private boolean dryRun; /** Sets whether the daemon is to run without making any * persistent state changes. * * @see MailResponder#isDryRun() */ public final void setDryRun( boolean newDryRun ) { dryRun = newDryRun; } /** The context for configuring access to the mail transfer server, * through which outgoing messages (such as replies to voters) are sent. */ public SMTPTransportX.ConfigurationContext transferService() { return transferService; } private final SMTPTransportX.ConfigurationContext transferService; } //// P r i v a t e /////////////////////////////////////////////////////////////////////// private static ArrayList addNonCriticalException( final Exception x, ArrayList xList ) { if( xList == null ) xList = new ArrayList( /*initial capacity*/2 ); xList.add( x ); return xList; } /** Matches and groups a single command argument, delimited by whitepace. */ private static final Pattern COMMAND_ARGUMENT_PATTERN = Pattern.compile( "\\s*(\\S+)" ); private final int inboxPollSleepSeconds = cc.getInboxPollSleepSeconds(); /** Matches an RFC 3282 language tag. Groups primary, secondary, * and remaining subtags. Leading delimiters ('-') of secondary * and remaining subtags are not stripped away. */ private static final Pattern LANGUAGE_TAG_PATTERN = Pattern.compile ( "^([A-Z]+)(-[a-z0-9]+)?((?:-[a-z0-9]+)+)?$", Pattern.CASE_INSENSITIVE ); private static final Logger logger = LoggerX.i( MailResponder.class ); private @ThreadRestricted("thread") final MailSender mailSender = new MailSender( cc.transferService() ); { mailSender.setDryRun( cc.isDryRun() ); } private @ThreadRestricted("thread") final Session mailSession; { final Properties p = new Properties( System.getProperties() ); { SMTPTransportX.SimpleAuthentication transferAuthentication = cc.transferService().getAuthenticationMethod(); if( transferAuthentication != null ) p.put( "mail.smtp.auth", "true" ); } mailSession = Session.getInstance( p ); } private static InternetAddress matchingAddress( final String[] envelopeBareAddressArray, final Address[] messageAddressArray ) { if( envelopeBareAddressArray == null || messageAddressArray == null ) return null; for( Address messageAddress : messageAddressArray ) { if( !( messageAddress instanceof InternetAddress )) continue; InternetAddress messageIAddress = (InternetAddress)messageAddress; for( String envelopeBareAddress : envelopeBareAddressArray ) { if( envelopeBareAddress.equals( messageIAddress.getAddress() )) { // if( matchingAddress == null ) matchingAddress = messageIAddress; // else if( !matchingAddress.equals( messageIAddress )) // { // throw new BadDeliveryException( // "multiple envelope 'Delivered-To' addresses " + Arrays.toString( envelopeBareAddressArray ) // + " match message 'To' addresses " + Arrays.toString( messageAddressArray )); // } ////// not using only 'Delivered-To' header now, so relax the sanity check: return messageIAddress; } } } return null; } private final MailMetaService metaService; /** Advances the command indeces to the next command in the message text. * Multiple commands are separated by blank lines (and so unaffected * by any line wrapping by the sender's mail client). * * @param ii command indeces from previous call, * or null to advance to the first command * @param messageText in which to seek the command * * @return Indeces of next command in a two-element array * (reusing ii if it was non-null). Element 0 is set to the index * of the first character of the first line of the next command; * or to beyond the message length, if there are no more commands. * Element 1 is set to the index of the end bound * (last plus 1) character. */ private static int[] nextCommand( int[] ii, final CharSequence messageText ) // will later be used by the authentication layer bypass, to parse the message for any command that requires authentication { if( ii == null ) ii = new int[2]; int i = ii[1]; final int iN = messageText.length(); for( int iLineStart = i; i < iN; ++i ) // find the start of the next command { final char ch = messageText.charAt( i ); if( ch == '\n' ) iLineStart = i + 1; else if( !Character.isWhitespace( ch )) { i = iLineStart; break; } } ii[0] = i; if( i < iN ) { int iLastNewline = -1; for( ;; ) // find the end bound of the command { ++i; if( i >= iN ) break; final char ch = messageText.charAt( i ); if( ch == '\n' ) { if( iLastNewline != -1 ) break; // two newlines in a row (or with only whitespace between them) iLastNewline = i; } else if( !Character.isWhitespace( ch )) iLastNewline = -1; } if( iLastNewline != -1 ) i = iLastNewline; } ii[1] = i; return ii; } /** Matches a signature delimiter. */ private static final Pattern SIGNATURE_DELIMITER_PATTERN = Pattern.compile( "(?m)^-- $" ); private final ThreadX thread = new ThreadX( MailResponder.this, "mail responder" ); // - R u n n a b l e ------------------------------------------------------------------ public void run() { assert Thread.currentThread() == thread; // private run() boolean toSleep = false; // initially, except for this small one: if( VOMailRD.i().stopLatch().tryAwait( 5, TimeUnit.SECONDS )) return; subserverRun.singleServiceLock().lock(); // no need to unlock, single access final LevelSwitchR lsrInboxPoll = new LevelSwitchR(); pollInbox: for( ;; ) { if( toSleep ) { logger.finest( "inboxPollSleepSeconds=" + inboxPollSleepSeconds ); if( VOMailRD.i().stopLatch().tryAwait( inboxPollSleepSeconds, TimeUnit.SECONDS )) break pollInbox; // because shutdown is holding for this thread, per tryJoin further above } else toSleep = true; // default for next time, to avoid busy looping Exception xInboxPoll = null; try { final Store store = mailSession.getStore( inboxStoreURLName() ); store.connect(); try { final Folder folder = StoreX.ensureDefaultFolder( store ); folder.open( Folder.READ_WRITE ); try { final int mN = folder.getMessageCount(); if( mN == 0 ) continue pollInbox; readInbox: for( int m = 1; m <= mN; ++m ) { if( VOMailRD.i().stopLatch().getCount() == 0 ) break pollInbox; // If more frequent checks are ever needed, to minimize stop lag, then divide message processing into smaller stages. And record stage state prior to each stage (message.setFlag with custom flags). See notebook 2007-10-10 (MCA). final Message message; try { message = folder.getMessage( m ); } catch( IndexOutOfBoundsException x ) { logger.warning( "another process has deleted a message in the mail store: " + store.toString() ); break readInbox; } if( message.isSet( Flags.Flag.DELETED )) continue readInbox; // unexpunged left over from last program run try { final LevelSwitchR lsr = new LevelSwitchR(); final TimeUnit unit = TimeUnit.SECONDS; int retryDelay = 10; runMessage: for( ;; ) { Exception x = run( message ); if( x == null ) break; logger.log( lsr.level(x,Level.INFO), "trouble reading message", x ); logger.info( "retry in " + unit + "=" + retryDelay + ", because: " + x ); if( VOMailRD.i().stopLatch().tryAwait( retryDelay, unit )) break pollInbox; if( unit.toMinutes(retryDelay) < 15 ) retryDelay <<= 1; // progressively longer, up to a limit } } catch( BadMessageException x ) { logger.log( Level.FINER, /*message*/"", x ); logger.info( "dropping this message, it caused: " + x ); } if( !dryRun ) message.setFlag( Flags.Flag.DELETED, true ); // done with that message } } finally{ folder.close( /*expunge those messages marked DELETED*/!dryRun ); } if( !dryRun ) toSleep = false; // real work was done, it's safe to skip the sleep } finally{ store.close(); } } catch( RuntimeException x ) { throw x; } catch( Exception x ) { xInboxPoll = x; } Level level = lsrInboxPoll.level( xInboxPoll, Level.WARNING ); // clear switch if null, per LevelSwitchR if( xInboxPoll != null ) logger.log( level, "trouble while polling the inbox", xInboxPoll ); } } /** Responds to a message. * * @return any soft exception, per CommandResponder.{@linkplain CommandResponder#respond(String[]) respond}; * or null if none occured * * @throws BadMessageException for unacceptable messages */ private Exception run( final Message message ) throws BadMessageException { ArrayList nonCriticalExceptionList = null; // lazilly created final MimeMessage mimeMessage; if( message instanceof MimeMessage ) mimeMessage = (MimeMessage)message; else { assert false : "all incoming messages are MIME"; mimeMessage = null; } // Parse the critical headers, needed to respond. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // final String fromAddressIndication; // address string(s), or some kind of placeholder final InternetAddress serviceEmail; final MimeMessage reply; final InternetAddress voterEmail; // if authenticated, else null final String voterEmailAuthenticationHeader = "X-TMDA-Confirm-Done"; try { logger.info( "inbox message: number=" + message.getMessageNumber() + ", Message-ID=" + PartX.getFirstHeader(message,"Message-ID") ); // /// fromAddressIndication /// // fromAddressIndication = Arrays.toString( message.getFrom() ); /// serviceEmail /// { final Address[] messageAddressArray = message.getRecipients( Message.RecipientType.TO ); final String[] envelopeOldBareAddressArray = message.getHeader( "Old-Delivered-To" ); // may be null InternetAddress matchingAddress = matchingAddress( envelopeOldBareAddressArray, messageAddressArray ); if( matchingAddress == null ) { final String[] envelopeBareAddressArray = message.getHeader( "Delivered-To" ); if( envelopeBareAddressArray == null ) { throw new BadDeliveryException( "missing envelope recipient header: 'Delivered-To'" ); } matchingAddress = matchingAddress( envelopeBareAddressArray, messageAddressArray ); if( matchingAddress == null ) { logger.info( "ignoring probable CC/BCC message, because no envelope 'Delivered-To' " + Arrays.toString(envelopeBareAddressArray) + " nor 'Old-Delivered-To' " + Arrays.toString(envelopeOldBareAddressArray) + " matches a message 'To' recipient " + Arrays.toString(messageAddressArray) ); return null; } } serviceEmail = matchingAddress; } logger.config( "request for electoral service: " + serviceEmail ); /// voterEmail /// final String[] returnPathArray = message.getHeader( "Return-Path" ); if( returnPathArray == null || returnPathArray.length == 0 ) { throw new BadDeliveryException( "envelope without sender address: 'Return-Path'" ); } if( PartX.getFirstHeader(message,voterEmailAuthenticationHeader) == null ) { if( returnPathArray.length > 1 ) throw new BadMessageException( "unknown sender, multiple headers 'Return-Path'" ); // under sender control final InternetAddress senderEmail = new InternetAddress( returnPathArray[0] ); if( "".equals( senderEmail.getAddress() )) { logger.fine( "ignoring bounce from 'Return-Path' " + Arrays.toString(returnPathArray) ); return null; } voterEmail = null; // unauthenticated, must be for the meta-service, or an unknown service } else // authenticated sender { if( returnPathArray.length > 1 ) // not under sender control, because TMDA's tmda-rfilter release_pending() ensures a single header here, so this should not happen { throw new BadDeliveryException( "envelope with multiple sender addresses: 'Return-Path'" ); } voterEmail = new InternetAddress( returnPathArray[0] ); // TMDA confirms the envelope sender address. ('Return-Path' is supposed to record it.) If tmda-filter-wrapper is used, then this envelope sender address is actually the first address of the 'From' header (and not necessarily the envelope sender, who might be merely a forwarder). In any case, voterEmail is the one that responded to the authentication challenge. } /// reply /// { final Message r = message.reply( /*replyToAll*/false ); // replyToAll not needed; voter may CC the original message, but I need not CC the reply if( !( r instanceof MimeMessage )) throw new VotorolaRuntimeException( "non-MIME reply created unexpectedly" ); // I depend on some of the convenience methods of MimeMessage, and assume it reply = (MimeMessage)r; } reply.setFrom( serviceEmail ); } catch( MessagingException x ) { throw new BadMessageException( x ); } // Limit replies when sender unauthenticated, in order to avoid looping // with another auto-responder, or sending too many replies to a falsified address. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - if( voterEmail == null ) // otherwise, TMDA has imposed its own limiting { // ignore anything that looks like an auto-responder - avoid looping // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` // to be coded later, as needed - for algorithm, see TMDA's // autorespond_to_sender(sender) in /usr/bin/tmda-rfilter - meantime, // fall back on rate-limiting: // // failsafe rate-limiting - limit loops - limit attacks on false address // ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` ` try { for( final Address address: reply.getAllRecipients() ) { FloatHolder load = loadOnAddressMap.get( address ); if( load == null ) { load = new FloatHolder(); loadOnAddressMap.put( address, load ); } else if( load.value > loadOnAddressMap.MAX_LOAD ) { throw new BadMessageException( "overloaded (" + load.value + ") by unauthenticated senders, temporarily dropping replies to address: " + address ); } load.value += 1F; } } catch( MessagingException x ) { throw new RuntimeException( x ); } // not expected } // Create the reply builder. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - final ReplyBuilder replyB; { Locale locale = null; try { if( mimeMessage != null ) { final String[] languageTag = mimeMessage.getContentLanguage(); if( languageTag != null && languageTag.length > 0 ) { Matcher m = LANGUAGE_TAG_PATTERN.matcher( languageTag[0] ); if( m.matches() ) { String primarySubtag = m.group( 1 ); String secondarySubtag = m.group( 2 ); String remainingSubtags = m.group( 3 ); if( remainingSubtags != null ) { locale = new Locale( primarySubtag, secondarySubtag.substring(1), remainingSubtags.substring(1).replace('-','_') ); } else if( secondarySubtag != null ) { locale = new Locale( primarySubtag, secondarySubtag.substring(1) ); } else locale = new Locale( primarySubtag ); } } } } catch( MessagingException x ) { nonCriticalExceptionList = addNonCriticalException( x, nonCriticalExceptionList ); } if( locale == null ) locale = Locale.getDefault(); replyB = new ReplyBuilder( locale ); } final BundleFormatter bunA = new BundleFormatter ( ResourceBundle.getBundle( "votorola.a.locale.A", replyB.locale() )); // Begin writing the reply. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - { String dateHead = null; try { dateHead = PartX.getFirstHeader( message, "Date" ); } catch( MessagingException x ) { nonCriticalExceptionList = addNonCriticalException( x, nonCriticalExceptionList ); } if( voterEmail == null ) { if( dateHead != null ) { replyB.lappendlnn( "a.mail.MailResponder.addr(1)", dateHead ); } else replyB.lappendlnn( "a.mail.MailResponder.addr(1Date)", new Date() ); } else { if( dateHead != null ) { replyB.lappendlnn( "a.mail.MailResponder.addr(1,2)", dateHead, voterEmail ); } else { replyB.lappendlnn( "a.mail.MailResponder.addr(1Date,2)", new Date(), voterEmail ); } } } // Look up the requested service. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - final ElectoralService electoralService; { ElectoralService s = null; try { s = subserverRun.electoralService( InternetAddressX.localPart( serviceEmail )); } catch( AddressException x ) { nonCriticalExceptionList = addNonCriticalException( x, nonCriticalExceptionList ); } // if( s == null ) throw new BadDeliveryException( "no such electoral service here: " + s ); if( s == null ) electoralService = metaService; else electoralService = s; } // Read the message text, and act on commands. // = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = { // Read the message text. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - final StringBuilder messageBuffer; // null if no message text { String string = null; try { final Part textPart = PartX.getPlainTextPart( message ); if( textPart != null ) { Object content = textPart.getContent(); if( content instanceof String ) string = (String)content; else logger.info( "ignoring text/plain part with improper content" ); // uncertain if this can ever occur } } catch( MessagingException x ) { nonCriticalExceptionList = addNonCriticalException( x, nonCriticalExceptionList ); } catch( IOException x ) { return x; } if( string == null ) messageBuffer = null; else messageBuffer = new StringBuilder( string ); } // Report any non-critical exceptions. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - if( nonCriticalExceptionList != null ) { final StringWriter stringWriter = new StringWriter(); final PrintWriter printWriter = new PrintWriter( stringWriter ); replyB.lappendlnn( "a.mail.MailResponder.nonCriticalX" ); for( Exception x : nonCriticalExceptionList ) { x.printStackTrace( printWriter ); printWriter.println(); } printWriter.flush(); replyB.append( stringWriter.toString() ); nonCriticalExceptionList = null; // done with it } // - - - if( messageBuffer == null ) replyB.lappendlnn( "a.mail.MailResponder.noTextPart" ); else { // Remove any message signature. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - { Matcher m = SIGNATURE_DELIMITER_PATTERN.matcher( messageBuffer ); if( m.find() ) messageBuffer.delete( m.start(), messageBuffer.length() ); } // Parse the commands out of the message. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - final CommandResponder.Session commandSession; { String voterEmailString = null; int trustLevel = 0; if( voterEmail != null ) { voterEmailString = voterEmail.getAddress(); try { trustLevel = subserverRun.register().getListNode( /*list ref*/null, voterEmailString ).trustLevel(); } catch( IOException x ) { return x; } catch( SQLException x ) { return x; } } commandSession = new CommandResponder.Session( voterEmailString, trustLevel, bunA, replyB ); } int commandCount = 0; final int[] ii = nextCommand( null, messageBuffer ); for( ;; ) // each command { replyB.resetFormattingToDefaults(); // fail-safe, in case responder poorly coded if( ii[0] >= messageBuffer.length() ) { if( commandCount == 0 ) { replyB.lappend( "a.mail.MailResponder.emptyText" ); final CommandResponder help = electoralService.responderByClassName( CR_Help.class.getName() ); if( help != null ) { replyB.append( " " ); replyB.lappendlnn( "a.mail.MailResponder.instructionsHelp" ); replyB.indent( 4 ); replyB.append( help.commandName( commandSession )); replyB.exdent( 4 ); } replyB.appendlnn(); } break; } ++commandCount; if( commandCount > 50 ) { replyB.append( '\n' ); replyB.lappendlnn( "a.mail.MailResponder.tooManyCommands" ); break; } if( replyB.length() > 3000000 ) { replyB.append( '\n' ); replyB.lappendlnn( "a.mail.MailResponder.replyTooLong" ); break; } // Look up the command responder. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - final String[] argArray; { final ArrayList argList = new ArrayList( /*initial capacity*/8 ); final Matcher m = COMMAND_ARGUMENT_PATTERN.matcher( messageBuffer ) .region( ii[0], ii[1] ); while( m.find() ) argList.add( m.group( 1 )); argArray = new String[argList.size()]; argList.toArray( argArray ); } final CommandResponder commandResponder = electoralService.responderForCommand( argArray, commandSession ); // Echo the command. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - replyB.setWrapping( false ); { int i = ii[0]; int iN = ii[1]; final int iCutoff; // to prevent echo of spam if( voterEmail == null ) { if( commandResponder == null ) iCutoff = i + 10; // almost certainly spam else iCutoff = i + 50; } else iCutoff = Integer.MAX_VALUE; // no cutoff char ch = '\n'; // prime it for(; i < iN; ++i ) { if( ch == '\n' ) replyB.append( "> " ); if( i >= iCutoff ) { replyB.append( "..." ); break; } ch = messageBuffer.charAt( i ); replyB.append( ch ); } } replyB.appendlnn().setWrapping( true ); // Act on the command. // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - try { Exception x = electoralService.dispatch ( argArray, commandSession, commandResponder ); if( x != null ) return x; } catch( CommandResponder.AnonymousIssueException x ) { throw new BadDeliveryException( "voter email address unconfirmed, missing header '" + voterEmailAuthenticationHeader + "'", x ); } // - - - nextCommand( ii, messageBuffer ); } } } // Transmit the reply. // = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = try { reply.setHeader( "X-Mailer", MailResponder.class.getName() ); reply.setText( replyB.chomplnn().toString(), "UTF-8" ); } catch( MessagingException x ) { throw new RuntimeException( x ); } // not expected return mailSender.trySend( reply, mailSession ); } // ==================================================================================== /** Thrown when an incoming message was badly delivered, * indicating a configuration problem. */ private static final class BadDeliveryException extends VotorolaRuntimeException { // public BadDeliveryException() {} // public BadDeliveryException( Throwable cause ) { super( cause ); } public BadDeliveryException( String message ) { super( message ); } public BadDeliveryException( String message, Throwable cause ) { super( message, cause ); } } // ==================================================================================== /** Thrown when an incoming message was composed in bad form, * and cannot be accepted. */ private static final class BadMessageException extends VotorolaException { // public BadMessageException() {} public BadMessageException( Throwable cause ) { super( cause ); } public BadMessageException( String message ) { super( message ); } // public BadMessageException( String message, Throwable cause ) { super( message, cause ); } } // ==================================================================================== @ThreadRestricted("thread") private final LoadOnAddressMap loadOnAddressMap = new LoadOnAddressMap(); /** Time sensitive map of load imposed by unauthenticated users on particular * addresses. The keys are the addresses that are burdened by the load, and the * values are the load in units of message equivalents (1.0 per message sent to the * address). Calls to get(key) will clear the map automatically, such that counts * are maintained for a limited time only. */ private static final class LoadOnAddressMap extends HashMap { private void accessed() { long now = System.currentTimeMillis(); if( now - lastClearTime > CLEAR_INTERVAL_MS ) { clear(); lastClearTime = now; } } private static final long CLEAR_INTERVAL_MS = 1000L * 60L * 60L; // ms * s * min = 1 hour private long lastClearTime = System.currentTimeMillis(); private static final float MAX_LOAD = 8F; // message equivalents // - M a p ------------------------------------------------------------------------ public FloatHolder get( Address key ) { accessed(); return super.get( key ); } } }