package votorola.s.mail; // Copyright 2007-2012, 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 javax.xml.ws.Holder; import votorola.a.*; import votorola.a.response.*; import votorola.a.response.line.*; import votorola.a.voter.*; import votorola.g.*; import votorola.g.hold.*; import votorola.g.lang.*; import votorola.g.locale.*; import votorola.g.mail.*; import votorola.g.option.*; import votorola.g.script.*; import votorola.g.util.*; import votorola.g.util.concurrent.*; import static votorola.a.response.line.CommandLine.COMMAND_ARGUMENT_PATTERN; /** The mail-based voter interface (mail interface). * * @see #main(String[]) * @see ../../s/manual.xht#mail */ public final @ThreadSafe class VOFaceMail implements Runnable, VoterInterface { /** Creates the mail interface from the command line, and starts it. * * @param argv the command line argument array. * * @see #i() * @see voface-mail */ public static void main( String[] argv ) { Thread.setDefaultUncaughtExceptionHandler( fatalExceptionHandler ); // early LoggerX.i(VOFaceMail.class).info( "voface-mail is running with arguments " + Arrays.toString( argv )); final Map optionMap = CommandLine.compileBaseOptions(); { final int aFirstNonOption = GetoptX.parse( "voface-mail", argv, optionMap ); if( aFirstNonOption < argv.length ) { System.err.println( GetoptX.createUnexpectedArgWarning( "voface-mail", argv[aFirstNonOption] )); System.err.println( GetoptX.createHelpPrompt( "voface-mail" )); System.exit( 1 ); } } if( optionMap.get("help").hasOccured() ) { System.out.print( "Usage: voface-mail\n" + "Await and respond to email messages from voters.\n" ); System.exit( 0 ); } try { LoggerX.i(VOFaceMail.class).info( "starting mail-based voter interface" ); new VOFaceMail(); // its thread will keep the VM running (if nothing else does) until Runtime.exit() is requested } catch( RuntimeException x ) { throw x; } // though it goes to fatalExceptionHandler regardless catch( Exception x ) { fatalExceptionHandler.uncaughtException( Thread.currentThread(), x ); // and exit } } /** The single instance of VOFaceMail, if it was created. */ static VOFaceMail i() { return instanceA.get(); } private static final AtomicReference instanceA = new AtomicReference(); private VOFaceMail() throws AddressException, IOException, ScriptException, SQLException, java.net.URISyntaxException { if( !instanceA.compareAndSet( /*expect*/null, VOFaceMail.this )) throw new IllegalStateException(); // Class.forName( "org.postgresql.Driver" ); // register the database driver // Connection databaseConnection = DriverManager.getConnection( url, username, password ); metaService = vsRun.init_ensureVoterService( new File( vsRun.voteServer().votorolaDirectory(), "mail" + File.separator + "mail-meta-service.js" ), MailMetaService.class ); thread.start(); 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 } // ```````````````````````````````````````````````````````````````````````````````````` // init for early use private final VoteServer.Run vsRun = new VoteServer( System.getProperty( "user.name" )).new Run( /*isSingleThreaded*/true ); ConstructionContext cc = ConstructionContext.configure( // nulled after init vsRun.voteServer(), compileConfigurationScript( vsRun.voteServer() )); // ------------------------------------------------------------------------------------ /** Executes the configuration script of the mail interface (without making any * configuration calls), thus compiling it for subsequent use. */ public static JavaScriptIncluder compileConfigurationScript( final VoteServer voteServer ) throws IOException, ScriptException { return new JavaScriptIncluder( new File( voteServer.votorolaDirectory(), "mail" + File.separator + "voface-mail.js" )); } /** 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 ConstructionContext#setInboxPollSleepSeconds(int) */ int inboxPollSleepSeconds() { return inboxPollSleepSeconds; } private final int inboxPollSleepSeconds = cc.getInboxPollSleepSeconds(); /** 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/vote-serverName/.mail"
      *   http://javamaildir.sourceforge.net/
      *
      * "pop3://vote-serverName:password@host:port" (not yet tested), per:
      *   http://java.sun.com/products/javamail/javadocs/com/sun/mail/pop3/package-summary.html
* * @see ConstructionContext#setInboxStoreURLName(String) */ URLName inboxStoreURLName() { return inboxStoreURLName; } private final URLName inboxStoreURLName = new URLName( cc.getInboxStoreURLName() ); /** Returns true if this interface 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 ConstructionContext#setDryRun(boolean) */ boolean isDryRun() { return dryRun; } private final boolean dryRun = cc.isDryRun(); /** The startup configuration file for this mail interface. The language is * JavaScript. There are restrictions on the {@linkplain * votorola.g.script.JavaScriptIncluder character encoding}. * * @see ../manual.xht#voface-mail.js */ File startupConfigurationFile() { return startupConfigurationFile; } private final File startupConfigurationFile = cc.startupConfigurationFile; // - V o t e r - I n t e r f a c e ---------------------------------------------------- /** @return descriptive email address of s, including a personal part * @see MailMetaService#serviceEmail(VoterService) */ public String serviceAccessDescriptor( final VoterService s ) { final String serviceEmail = metaService.serviceEmail( s ); try { return new InternetAddress( serviceEmail, s.title() ).toUnicodeString(); } catch( UnsupportedEncodingException x ) { assert false; return serviceEmail; } } // ==================================================================================== /** A context for configuring the mail interface. The interface is configured by the * responder's {@linkplain #startupConfigurationFile startup configuration file}, * which contains a script (s) for that purpose. During construction of the * interface, an instance of this context (mailCC) is passed to s, via * s::constructingVOFaceMail(mailCC). */ public static @ThreadSafe final class ConstructionContext { /** Constructs the complete configuration of the mail interface. * * @param s the compiled startup configuration script. */ public static ConstructionContext configure( final VoteServer voteServer, final JavaScriptIncluder s ) throws ScriptException { final ConstructionContext cc = new ConstructionContext( voteServer, s ); s.invokeKnownFunction( "constructingVOFaceMail", cc ); return cc; } private ConstructionContext( VoteServer voteServer, JavaScriptIncluder s ) { this.s = s; startupConfigurationFile = s.scriptFile(); inboxStoreURLName = "maildir:/home/" + voteServer.name() + "/Maildir"; transferService = new SMTPTransportX.ConstructionContext( startupConfigurationFile ); } private final File startupConfigurationFile; private final JavaScriptIncluder s; // -------------------------------------------------------------------------------- /** @see VOFaceMail#inboxPollSleepSeconds() * @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 VOFaceMail#inboxPollSleepSeconds() */ @ThreadRestricted("constructor") public void setInboxPollSleepSeconds( int inboxPollSleepSeconds ) { this.inboxPollSleepSeconds = inboxPollSleepSeconds; } /** @see VOFaceMail#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 VoteServer#name() vote-server-name}/Maildir". * *

"maildir:/home/" + voteServer.{@linkplain * VoteServer#name() name}() + "/Maildir"

* * @see VOFaceMail#inboxStoreURLName() */ @ThreadRestricted("constructor") public void setInboxStoreURLName( String inboxStoreURLName ) { this.inboxStoreURLName = inboxStoreURLName; } /** @see VOFaceMail#isDryRun() * @see #setDryRun(boolean) */ public final boolean isDryRun() { return dryRun; }; private boolean dryRun; /** Sets whether the interface is to run without making any * persistent state changes. * * @see VOFaceMail#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.ConstructionContext transferService() { return transferService; } private final SMTPTransportX.ConstructionContext transferService; } //// P r i v a t e /////////////////////////////////////////////////////////////////////// private static final Thread.UncaughtExceptionHandler fatalExceptionHandler = new ThreadX.UncaughtExceptionLogger( LoggerX.SEVERE ) { public void uncaughtException( Thread thread, final Throwable t ) { super.uncaughtException( thread, t ); // log the full trace System.err.println( "Fatal error" + Votorola.unmessagedDetails(t) + ": " + t ); // give the user a brief message System.exit( 1 ); // rather than risk running with a dead thread } }; /** 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( VOFaceMail.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 the command indeces from the previous call, or null to advance to * the first command. * @param messageText the text 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)^-- $" ); /** Spool unwound just before this run ends, at VM shut-down. */ private final Spool spool = new SpoolT(); { Thread shutdownThread = new Thread() { public void run() { // LoggerX.i(VOFaceMail.class).info( "stopping mail-based voter interface" ); ///// logging service unreliable, at this stage stopLatch.countDown(); final CatcherP catcher = CatcherP.i(); spool.unwind( catcher ); // throw new Error( "test error in shutdown thread" ); } }; shutdownThread.setUncaughtExceptionHandler( new ThreadX.UncaughtExceptionPrinter() ); // instead of this run's setDefaultUncaughtExceptionHandler, which depends on the logger Runtime.getRuntime().addShutdownHook( shutdownThread ); // spool.add( new Hold() // { // public @ThreadSafe void release() { throw new RuntimeException( "test exception during shutdown" ); } // }); } /** A latch that zeroes when this interface is stopping. */ CountDownLatchX stopLatch() { return stopLatch; } private CountDownLatchX stopLatch = new CountDownLatchX( 1 ); private final ThreadX thread = new ThreadX( VOFaceMail.this, "mail interface" ); // - 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( stopLatch().tryAwait( 5, TimeUnit.SECONDS )) return; vsRun.singleServiceLock().lock(); // no need to unlock, single access final LevelSwitchR lsrInboxPoll = new LevelSwitchR(); pollInbox: for( ;; ) { if( toSleep ) { logger.finest( "inboxPollSleepSeconds=" + inboxPollSleepSeconds ); if( stopLatch().tryAwait( inboxPollSleepSeconds, TimeUnit.SECONDS )) break pollInbox; // because shutdown is holding for this thread, per tryJoin farther 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 ); /// for Maildir, that now gives the 'cur' directory. This gives 'new': final Folder folder = store.getDefaultFolder(); 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( 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( 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 { List nonCriticalXList = null; // lazily 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 IDPair voter; 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 voter 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. } voter = IDPair.fromEmail( voterEmail.getAddress() ); /// 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" ); // we depend on convenience methods of MimeMessage 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() ) { Holder loadH = loadOnAddressMap.get( address ); if( loadH == null ) { loadH = new Holder(); loadOnAddressMap.put( address, loadH ); } else if( loadH.value > LoadOnAddressMap.MAX_LOAD ) { throw new BadMessageException( "overloaded (" + loadH.value + ") by unauthenticated senders, temporarily dropping replies to address: " + address ); } loadH.value += 1F; } } catch( MessagingException x ) { throw new RuntimeException( x ); } } // 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 ) { final 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 ) { nonCriticalXList = ThrowableX.listedThrowable( x, nonCriticalXList ); } if( locale == null ) locale = Locale.getDefault(); replyB = new ReplyBuilder( locale ); } final BundleFormatter bunA = new BundleFormatter( ResourceBundle.getBundle( "votorola.a.locale.A", replyB.locale(), new BundleControlU() )); // Begin writing the reply // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - { String dateHead = null; try { dateHead = PartX.getFirstHeader( message, "Date" ); } catch( MessagingException x ) { nonCriticalXList = ThrowableX.listedThrowable( x, nonCriticalXList ); } if( voterEmail == null ) { if( dateHead != null ) { replyB.lappendlnn( "s.mail.VOFaceMail.addr(1)", dateHead ); } else replyB.lappendlnn( "s.mail.VOFaceMail.addr(1Date)", new Date() ); } else { if( dateHead != null ) { replyB.lappendlnn( "s.mail.VOFaceMail.addr(1,2)", dateHead, voterEmail ); } else { replyB.lappendlnn( "s.mail.VOFaceMail.addr(1Date,2)", new Date(), voterEmail ); } } } // Look up the requested service // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - final VoterService voterService; try { final String name = metaService.serviceName( serviceEmail ); if( VoterService.isNonPoll( name )) { final VoterService s = vsRun.voterService( name); // if( s == null ) throw new BadDeliveryException( "no such voter service here: " + serviceName ); voterService = s == null? metaService: s; } else voterService = vsRun.scopePoll().ensurePoll( name ); } catch( RuntimeException x ) { throw x; } catch( Exception x ) { return x; } // 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 ) { nonCriticalXList = ThrowableX.listedThrowable( x, nonCriticalXList ); } catch( UnsupportedEncodingException x ) { nonCriticalXList = ThrowableX.listedThrowable( x, nonCriticalXList ); } // message with a strange character set catch( IOException x ) { return x; } if( string == null ) messageBuffer = null; else messageBuffer = new StringBuilder( string ); } // Report any non-critical exceptions // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - if( nonCriticalXList != null ) { final StringWriter stringWriter = new StringWriter(); final PrintWriter printWriter = new PrintWriter( stringWriter ); replyB.lappendlnn( "s.mail.VOFaceMail.nonCriticalX" ); for( Throwable x : nonCriticalXList ) { x.printStackTrace( printWriter ); printWriter.println(); } printWriter.flush(); replyB.append( stringWriter.toString() ); nonCriticalXList = null; // done with it } // - - - if( messageBuffer == null ) replyB.lappendlnn( "s.mail.VOFaceMail.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; { int trustLevel = 0; if( voter != null ) { try { trustLevel = vsRun.trustserver().getTraceNode( /*list ref*/null, voter ).trustLevel(); } catch( IOException x ) { return x; } catch( SQLException x ) { return x; } } commandSession = new CommandResponder.Session( VOFaceMail.this, voter.email(), 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( "s.mail.VOFaceMail.emptyText" ); final CommandResponder help = voterService.responderByClassName( CR_Help.class.getName() ); if( help != null ) { replyB.append( " " ); replyB.lappendlnn( "s.mail.VOFaceMail.instructionsHelp" ); replyB.indent( 4 ); replyB.append( help.commandName( commandSession )); replyB.exdent( 4 ); } replyB.appendlnn(); } break; } ++commandCount; if( commandCount > 50 ) { replyB.append( '\n' ); replyB.lappendlnn( "s.mail.VOFaceMail.tooManyCommands" ); break; } if( replyB.length() > 3000000 ) { replyB.append( '\n' ); replyB.lappendlnn( "s.mail.VOFaceMail.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 = voterService.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 = voterService.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", VOFaceMail.class.getName() ); reply.setText( replyB.chomplnn().toString(), "UTF-8" ); } catch( MessagingException x ) { throw new RuntimeException( x ); } 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 // - S c e n e -------------------------------------------------------------------- public Holder get( Address key ) { accessed(); return super.get( key ); } } }