/* * Created on 04.11.2004 * * This file is part of susimail project, see http://susi.i2p/ * * Copyright (C) 2004-2005 <susi23@mail.i2p> * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA * * $Revision: 1.2 $ */ package i2p.susi.webmail; import i2p.susi.debug.Debug; import i2p.susi.util.Config; import i2p.susi.util.Folder; import i2p.susi.util.ReadBuffer; import i2p.susi.webmail.Messages; import i2p.susi.webmail.encoding.DecodingException; import i2p.susi.webmail.encoding.Encoding; import i2p.susi.webmail.encoding.EncodingException; import i2p.susi.webmail.encoding.EncodingFactory; import i2p.susi.webmail.pop3.POP3MailBox; import i2p.susi.webmail.smtp.SMTPClient; import java.io.BufferedReader; import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileFilter; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.PrintWriter; import java.io.Serializable; import java.io.StringWriter; import java.io.UnsupportedEncodingException; import java.text.Collator; import java.util.ArrayList; import java.util.Comparator; import java.util.Date; import java.util.Enumeration; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Properties; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import javax.servlet.http.HttpSessionBindingEvent; import javax.servlet.http.HttpSessionBindingListener; import net.i2p.CoreVersion; import net.i2p.I2PAppContext; import net.i2p.data.DataHelper; import net.i2p.servlet.RequestWrapper; import net.i2p.util.Translate; /** * @author susi23 */ public class WebMail extends HttpServlet { /* * set to true, if its a release build */ private static final boolean RELEASE; /* * increase version number for every release */ private static final int version = 13; private static final long serialVersionUID = 1L; private static final String LOGIN_NONCE = Long.toString(I2PAppContext.getGlobalContext().random().nextLong()); private static final String DEFAULT_HOST = "127.0.0.1"; private static final int DEFAULT_POP3PORT = 7660; private static final int DEFAULT_SMTPPORT = 7659; private static final int STATE_AUTH = 1; private static final int STATE_LIST = 2; private static final int STATE_SHOW = 3; private static final int STATE_NEW = 4; private static final int STATE_CONFIG = 5; // TODO generate from servlet name to allow for renaming or multiple instances private static final String myself = "/susimail/susimail"; /* * form keys on login page */ private static final String LOGIN = "login"; private static final String OFFLINE = "offline"; private static final String USER = "user"; private static final String PASS = "pass"; private static final String HOST = "host"; private static final String POP3 = "pop3"; private static final String SMTP = "smtp"; /* * button names */ private static final String LOGOUT = "logout"; private static final String RELOAD = "reload"; private static final String SAVE = "save"; private static final String SAVE_AS = "saveas"; private static final String REFRESH = "refresh"; private static final String CONFIGURE = "configure"; private static final String NEW = "new"; private static final String REPLY = "reply"; private static final String REPLYALL = "replyall"; private static final String FORWARD = "forward"; private static final String DELETE = "delete"; private static final String REALLYDELETE = "really_delete"; private static final String SHOW = "show"; private static final String DOWNLOAD = "download"; private static final String RAW_ATTACHMENT = "att"; private static final String SUSI_NONCE = "susiNonce"; private static final String MARKALL = "markall"; private static final String CLEAR = "clearselection"; private static final String INVERT = "invertselection"; private static final String PREVPAGE = "prevpage"; private static final String NEXTPAGE = "nextpage"; private static final String FIRSTPAGE = "firstpage"; private static final String LASTPAGE = "lastpage"; private static final String PAGESIZE = "pagesize"; private static final String SETPAGESIZE = "setpagesize"; private static final String SEND = "send"; private static final String CANCEL = "cancel"; private static final String DELETE_ATTACHMENT = "delete_attachment"; private static final String NEW_FROM = "new_from"; private static final String NEW_SUBJECT = "new_subject"; private static final String NEW_TO = "new_to"; private static final String NEW_CC = "new_cc"; private static final String NEW_BCC = "new_bcc"; private static final String NEW_TEXT = "new_text"; private static final String NEW_FILENAME = "new_filename"; private static final String NEW_UPLOAD = "new_upload"; private static final String NEW_BCC_TO_SELF = "new_bcc_to_self"; private static final String LIST = "list"; private static final String PREV = "prev"; private static final String NEXT = "next"; private static final String SORT_ID = "sort_id"; private static final String SORT_SENDER = "sort_sender"; private static final String SORT_SUBJECT = "sort_subject"; private static final String SORT_DATE = "sort_date"; private static final String SORT_SIZE = "sort_size"; private static final String CONFIG_TEXT = "config_text"; private static final boolean SHOW_HTML = true; private static final boolean TEXT_ONLY = false; /* * name of configuration properties */ private static final String CONFIG_HOST = "host"; private static final String CONFIG_PORTS_FIXED = "ports.fixed"; private static final String CONFIG_PORTS_POP3 = "ports.pop3"; private static final String CONFIG_PORTS_SMTP = "ports.smtp"; private static final String CONFIG_SENDER_FIXED = "sender.fixed"; private static final String CONFIG_SENDER_DOMAIN = "sender.domain"; private static final String CONFIG_SENDER_NAME = "sender.name"; private static final String CONFIG_COMPOSER_COLS = "composer.cols"; private static final String CONFIG_COMPOSER_ROWS = "composer.rows"; private static final String CONFIG_BCC_TO_SELF = "composer.bcc.to.self"; static final String CONFIG_LEAVE_ON_SERVER = "pop3.leave.on.server"; public static final String CONFIG_BACKGROUND_CHECK = "pop3.check.enable"; public static final String CONFIG_CHECK_MINUTES = "pop3.check.interval.minutes"; public static final String CONFIG_IDLE_SECONDS = "pop3.idle.timeout.seconds"; private static final String CONFIG_DEBUG = "debug"; private static final String RC_PROP_THEME = "routerconsole.theme"; private static final String RC_PROP_UNIVERSAL_THEMING = "routerconsole.universal.theme"; private static final String RC_PROP_FORCE_MOBILE_CONSOLE = "routerconsole.forceMobileConsole"; private static final String CONFIG_THEME = "theme"; private static final String DEFAULT_THEME = "light"; private static final String spacer = "   "; private static final String thSpacer = "<th> </th>\n"; private static final String CONSOLE_BUNDLE_NAME = "net.i2p.router.web.messages"; static { Config.setPrefix( "susimail" ); RELEASE = !Boolean.parseBoolean(Config.getProperty(CONFIG_DEBUG)); Debug.setLevel( RELEASE ? Debug.ERROR : Debug.DEBUG ); } /** * sorts Mail objects by id field * * @author susi */ /**** private static class IDSorter implements Comparator<String> { private final MailCache mailCache; public IDSorter( MailCache mailCache ) { this.mailCache = mailCache; } public int compare(String arg0, String arg1) { Mail a = mailCache.getMail( arg0, MailCache.FETCH_HEADER ); Mail b = mailCache.getMail( arg1, MailCache.FETCH_HEADER ); if (a == null) return (b == null) ? 0 : 1; if (b == null) return -1; return a.id - b.id; } } ****/ /** * Base for the various sorters * * @since 0.9.13 */ private abstract static class SorterBase implements Comparator<String>, Serializable { private final MailCache mailCache; /** * Set MailCache object, where to get Mails from * @param mailCache */ protected SorterBase( MailCache mailCache ) { this.mailCache = mailCache; } /** * Gets mail from the cache, checks for null, then compares */ public int compare(String arg0, String arg1) { Mail a = mailCache.getMail( arg0, MailCache.FetchMode.CACHE_ONLY ); Mail b = mailCache.getMail( arg1, MailCache.FetchMode.CACHE_ONLY ); if (a == null) return (b == null) ? 0 : 1; if (b == null) return -1; int rv = compare(a, b); if (rv != 0) return rv; return fallbackCompare(a, b); } /** * @param a non-null * @param b non-null */ protected abstract int compare(Mail a, Mail b); /** * @param a non-null * @param b non-null */ private int fallbackCompare(Mail a, Mail b) { return DateSorter.scompare(a, b); } } /** * sorts Mail objects by sender field * * @author susi */ private static class SenderSorter extends SorterBase { private final Comparator<Object> collator = Collator.getInstance(); public SenderSorter( MailCache mailCache ) { super(mailCache); } protected int compare(Mail a, Mail b) { String as = a.sender.replace("\"", "").replace("<", "").replace(">", ""); String bs = b.sender.replace("\"", "").replace("<", "").replace(">", ""); return collator.compare(as, bs); } } /** * sorts Mail objects by subject field * @author susi */ private static class SubjectSorter extends SorterBase { private final Comparator<Object> collator = Collator.getInstance(); public SubjectSorter( MailCache mailCache ) { super(mailCache); } protected int compare(Mail a, Mail b) { String as = a.formattedSubject; String bs = b.formattedSubject; if (as.toLowerCase().startsWith("re:")) { as = as.substring(3).trim(); } else if (as.toLowerCase().startsWith("fwd:")) { as = as.substring(4).trim(); } else { String xre = _t("Re:").toLowerCase(); if (as.toLowerCase().startsWith(xre)) { as = as.substring(xre.length()).trim(); } else { String xfwd = _t("Fwd:").toLowerCase(); if (as.toLowerCase().startsWith(xfwd)) as = as.substring(xfwd.length()).trim(); } } if (bs.toLowerCase().startsWith("re:")) { bs = bs.substring(3).trim(); } else if (bs.toLowerCase().startsWith("fwd:")) { bs = bs.substring(4).trim(); } else { String xre = _t("Re:").toLowerCase(); if (bs.toLowerCase().startsWith(xre)) { bs = bs.substring(xre.length()).trim(); } else { String xfwd = _t("Fwd:").toLowerCase(); if (bs.toLowerCase().startsWith(xfwd)) bs = bs.substring(xfwd.length()).trim(); } } return collator.compare(as, bs); } } /** * sorts Mail objects by date field * @author susi */ private static class DateSorter extends SorterBase { public DateSorter( MailCache mailCache ) { super(mailCache); } protected int compare(Mail a, Mail b) { return scompare(a, b); } /** * Use as fallback in other sorters * @param a non-null * @param b non-null */ public static int scompare(Mail a, Mail b) { return a.date != null ? ( b.date != null ? a.date.compareTo( b.date ) : -1 ) : ( b.date != null ? 1 : 0 ); } } /** * sorts Mail objects by message size * @author susi */ private static class SizeSorter extends SorterBase { public SizeSorter( MailCache mailCache ) { super(mailCache); } protected int compare(Mail a, Mail b) { return a.getSize() - b.getSize(); } } /** * data structure to hold any persistent data (to store them in session dictionary) * @author susi */ private static class SessionObject implements HttpSessionBindingListener, NewMailListener { boolean pageChanged, markAll, clear, invert; int state, smtpPort; POP3MailBox mailbox; MailCache mailCache; Folder<String> folder; String user, pass, host, error, info; String replyTo, replyCC; String subject, body, showUIDL; public String sentMail; public ArrayList<Attachment> attachments; public boolean reallyDelete; String themePath, imgPath; boolean isMobile; boolean bccToSelf; private final List<String> nonces; private static final int MAX_NONCES = 15; SessionObject() { state = STATE_AUTH; bccToSelf = Boolean.parseBoolean(Config.getProperty( CONFIG_BCC_TO_SELF, "true" )); nonces = new ArrayList<String>(MAX_NONCES + 1); } /** @since 0.9.13 */ public void valueBound(HttpSessionBindingEvent event) {} /** * Close the POP3 socket if still open * @since 0.9.13 */ public void valueUnbound(HttpSessionBindingEvent event) { Debug.debug(Debug.DEBUG, "Session unbound: " + event.getSession().getId()); POP3MailBox mbox = mailbox; if (mbox != null) { mbox.destroy(); mailbox = null; } } /** * Relay from the checker to the webmail session object, * which relays to MailCache, which will fetch the mail from us * in a big circle * * @since 0.9.13 */ public void foundNewMail() { MailCache mc = mailCache; Folder<String> f = folder; if (mc != null && f != null) { String[] uidls = mc.getUIDLs(); f.setElements(uidls); } } /** @since 0.9.27 */ public void addNonce(String nonce) { synchronized(nonces) { nonces.add(0, nonce); if (nonces.size() > MAX_NONCES) { nonces.remove(MAX_NONCES); } } } /** @since 0.9.27 */ public boolean isValidNonce(String nonce) { if (state == STATE_AUTH && LOGIN_NONCE.equals(nonce)) return true; synchronized(nonces) { return nonces.contains(nonce); } } } /** * returns html string of a form button with name and label * * @param name * @param label * @return html string */ private static String button( String name, String label ) { StringBuilder buf = new StringBuilder(128); buf.append("<input type=\"submit\" class=\"").append(name).append("\" name=\"") .append(name).append("\" value=\"").append(label).append('"'); if (name.equals(SEND) || name.equals(CANCEL) || name.equals(DELETE_ATTACHMENT) || name.equals(NEW_UPLOAD)) buf.append(" onclick=\"cancelPopup()\""); buf.append('>'); return buf.toString(); } /** * returns html string of a disabled form button with name and label * * @param name * @param label * @return html string */ private static String button2( String name, String label ) { return "<input type=\"submit\" name=\"" + name + "\" value=\"" + label + "\" disabled>"; } /** * returns a html string of the label and two imaged links using the parameter name * (used for sorting buttons in folder view) * * @param name * @param label * @return the string */ private static String sortHeader( String name, String label, String imgPath, String currentName, Folder.SortOrder currentOrder) { StringBuilder buf = new StringBuilder(128); buf.append(label).append("  "); if (name.equals(currentName) && currentOrder == Folder.SortOrder.UP) { buf.append("<img class=\"sort\" src=\"").append(imgPath).append("3up.png\" border=\"0\" alt=\"^\">\n"); } else { buf.append("<a class=\"sort\" href=\"").append(myself).append('?').append(name).append("=up\">"); buf.append("<img class=\"sort\" src=\"").append(imgPath).append("3up.png\" border=\"0\" alt=\"^\" style=\"opacity: 0.4;\">"); buf.append("</a>\n"); } if (name.equals(currentName) && currentOrder == Folder.SortOrder.DOWN) { buf.append("<img class=\"sort\" src=\"").append(imgPath).append("3down.png\" border=\"0\" alt=\"v\">"); } else { buf.append("<a class=\"sort\" href=\"").append(myself).append('?').append(name).append("=down\">"); buf.append("<img class=\"sort\" src=\"").append(imgPath).append("3down.png\" border=\"0\" alt=\"v\" style=\"opacity: 0.4;\">"); buf.append("</a>"); } return buf.toString(); } /** * check, if a given button "was pressed" in the received http request * * @param request * @param key * @return true if pressed */ private static boolean buttonPressed( RequestWrapper request, String key ) { String value = request.getParameter( key ); return value != null && value.length() > 0; } /** * recursively render all mail body parts * * 1. if type is multipart/alternative, look for text/plain section and ignore others * 2. if type is multipart/*, recursively call all these parts * 3. if type is text/plain (or mail is not mime), print out * 4. in all other cases print out message, that part is not displayed * * @param out * @param mailPart * @param level is increased by recursively calling sub parts */ private static void showPart( PrintWriter out, MailPart mailPart, int level, boolean html ) { String br = html ? "<br>\r\n" : "\r\n"; if( html ) { out.println( "<!-- " ); out.println( "Debug: Mail Part headers follow"); for( int i = 0; i < mailPart.headerLines.length; i++ ) { // fix Content-Type: multipart/alternative; boundary="----------8CDE39ECAF2633" out.println( mailPart.headerLines[i].replace("--", "—") ); } out.println( "-->" ); } if( mailPart.multipart ) { if( mailPart.type.equals("multipart/alternative")) { MailPart chosen = null; for( MailPart subPart : mailPart.parts ) { if( subPart.type != null && subPart.type.equals("text/plain")) chosen = subPart; } if( chosen != null ) { showPart( out, chosen, level + 1, html ); return; } } for( MailPart part : mailPart.parts ) { showPart( out, part, level + 1, html ); } } else if( mailPart.message ) { for( MailPart part : mailPart.parts ) { showPart( out, part, level + 1, html ); } } else { boolean showBody = false; boolean prepareAttachment = false; String reason = ""; StringBuilder body = null; String ident = quoteHTML( ( mailPart.description != null ? mailPart.description + ", " : "" ) + ( mailPart.filename != null ? mailPart.filename + ", " : "" ) + ( mailPart.name != null ? mailPart.name + ", " : "" ) + ( mailPart.type != null ? '(' + mailPart.type + ')' : _t("unknown") ) ); if( level == 0 && mailPart.version == null ) { /* * not a MIME mail, so simply print it literally */ showBody = true; } if( showBody == false && mailPart.type != null ) { if( mailPart.type.equals("text/plain")) { showBody = true; } else prepareAttachment = true; } if( showBody ) { String charset = mailPart.charset; if( charset == null ) { charset = "US-ASCII"; // don't show this in text mode which is used to include the mail in the reply or forward if (html) reason += _t("Warning: no charset found, fallback to US-ASCII.") + br; } try { ReadBuffer decoded = mailPart.decode(0); BufferedReader reader = new BufferedReader( new InputStreamReader( new ByteArrayInputStream( decoded.content, decoded.offset, decoded.length ), charset ) ); body = new StringBuilder(); String line; while( ( line = reader.readLine() ) != null ) { body.append( quoteHTML( line ) ); body.append( br ); } } catch( UnsupportedEncodingException uee ) { showBody = false; reason = _t("Charset \\''{0}\\'' not supported.", quoteHTML( mailPart.charset )) + br; } catch (IOException e1) { showBody = false; reason += _t("Part ({0}) not shown, because of {1}", ident, e1.toString()) + br; } } if( html ) out.println( "<tr class=\"mailbody\"><td colspan=\"2\" align=\"center\">" ); if( reason != null && reason.length() > 0 ) { if( html ) out.println( "<p class=\"info\">"); out.println( reason ); if( html ) out.println( "</p>" ); } if( showBody ) { if( html ) out.println( "<p class=\"mailbody\">" ); out.println( body.toString() ); if( html ) out.println( "</p>" ); } if( prepareAttachment ) { if( html ) { out.println( "<hr><p class=\"mailbody\">" ); String type = mailPart.type; if (type != null && type.startsWith("image/")) { // we at least show images safely... out.println("<img src=\"" + myself + "?" + RAW_ATTACHMENT + "=" + mailPart.hashCode() + "\">"); } else if (type != null && ( // type list from snark type.startsWith("audio/") || type.equals("application/ogg") || type.startsWith("video/") || type.equals("application/zip") || type.equals("application/x-gtar") || type.equals("application/compress") || type.equals("application/gzip") || type.equals("application/x-7z-compressed") || type.equals("application/x-rar-compressed") || type.equals("application/x-tar") || type.equals("application/x-bzip2"))) { out.println( "<a href=\"" + myself + "?" + RAW_ATTACHMENT + "=" + mailPart.hashCode() + "\">" + _t("Download attachment {0}", ident) + "</a>"); } else { out.println( "<a target=\"_blank\" href=\"" + myself + "?" + DOWNLOAD + "=" + mailPart.hashCode() + "\">" + _t("Download attachment {0}", ident) + "</a>" + " (" + _t("File is packed into a zipfile for security reasons.") + ')'); } out.println( "</p>" ); } else { out.println( _t("Attachment ({0}).", ident) ); } } if( html ) out.println( "</td></tr>" ); } } /** * prepare line for presentation between html tags * * - quote html tags * * @param line * @return escaped string */ static String quoteHTML( String line ) { if( line != null ) line = DataHelper.escapeHTML(line); else line = ""; return line; } /** * * @param sessionObject * @param request */ private static void processLogin( SessionObject sessionObject, RequestWrapper request ) { if( sessionObject.state == STATE_AUTH ) { String user = request.getParameter( USER ); String pass = request.getParameter( PASS ); String host = request.getParameter( HOST ); String pop3Port = request.getParameter( POP3 ); String smtpPort = request.getParameter( SMTP ); boolean fixedPorts = Boolean.parseBoolean(Config.getProperty( CONFIG_PORTS_FIXED, "true" )); if (fixedPorts) { host = Config.getProperty( CONFIG_HOST, DEFAULT_HOST ); pop3Port = Config.getProperty( CONFIG_PORTS_POP3, "" + DEFAULT_POP3PORT ); smtpPort = Config.getProperty( CONFIG_PORTS_SMTP, "" + DEFAULT_SMTPPORT ); } boolean doContinue = true; /* * security :( */ boolean offline = buttonPressed(request, OFFLINE); if (buttonPressed(request, LOGIN) || offline) { if( user == null || user.length() == 0 ) { sessionObject.error += _t("Need username for authentication.") + '\n'; doContinue = false; } if( pass == null || pass.length() == 0 ) { sessionObject.error += _t("Need password for authentication.") + '\n'; doContinue = false; } if( host == null || host.length() == 0 ) { sessionObject.error += _t("Need hostname for connect.") + '\n'; doContinue = false; } int pop3PortNo = 0; if( pop3Port == null || pop3Port.length() == 0 ) { sessionObject.error += _t("Need port number for pop3 connect.") + '\n'; doContinue = false; } else { try { pop3PortNo = Integer.parseInt( pop3Port ); if( pop3PortNo < 0 || pop3PortNo > 65535 ) { sessionObject.error += _t("POP3 port number is not in range 0..65535.") + '\n'; doContinue = false; } } catch( NumberFormatException nfe ) { sessionObject.error += _t("POP3 port number is invalid.") + '\n'; doContinue = false; } } int smtpPortNo = 0; if( smtpPort == null || smtpPort.length() == 0 ) { sessionObject.error += _t("Need port number for smtp connect.") + '\n'; doContinue = false; } else { try { smtpPortNo = Integer.parseInt( smtpPort ); if( smtpPortNo < 0 || smtpPortNo > 65535 ) { sessionObject.error += _t("SMTP port number is not in range 0..65535.") + '\n'; doContinue = false; } } catch( NumberFormatException nfe ) { sessionObject.error += _t("SMTP port number is invalid.") + '\n'; doContinue = false; } } if( doContinue ) { POP3MailBox mailbox = new POP3MailBox( host, pop3PortNo, user, pass ); if (offline || mailbox.connectToServer()) { sessionObject.mailbox = mailbox; sessionObject.user = user; sessionObject.pass = pass; sessionObject.host = host; sessionObject.smtpPort = smtpPortNo; sessionObject.state = STATE_LIST; MailCache mc = new MailCache(mailbox, host, pop3PortNo, user, pass); sessionObject.mailCache = mc; sessionObject.folder = new Folder<String>(); if (!offline) { // prime the cache, request all headers at once // otherwise they are pulled one at a time by sortBy() below mc.getMail(MailCache.FetchMode.HEADER); } // get through cache so we have the disk-only ones too String[] uidls = mc.getUIDLs(); sessionObject.folder.setElements(uidls); //sessionObject.folder.addSorter( SORT_ID, new IDSorter( sessionObject.mailCache ) ); sessionObject.folder.addSorter( SORT_SENDER, new SenderSorter( sessionObject.mailCache ) ); sessionObject.folder.addSorter( SORT_SUBJECT, new SubjectSorter( sessionObject.mailCache ) ); sessionObject.folder.addSorter( SORT_DATE, new DateSorter( sessionObject.mailCache ) ); sessionObject.folder.addSorter( SORT_SIZE, new SizeSorter( sessionObject.mailCache ) ); // reverse sort, latest mail first sessionObject.folder.setSortingDirection(Folder.SortOrder.UP); sessionObject.folder.sortBy(SORT_DATE); sessionObject.reallyDelete = false; if (offline) Debug.debug(Debug.DEBUG, "OFFLINE MODE"); else Debug.debug(Debug.DEBUG, "CONNECTED, YAY"); // we do this after the initial priming above mailbox.setNewMailListener(sessionObject); } else { sessionObject.error += mailbox.lastError(); Debug.debug(Debug.DEBUG, "LOGIN FAIL, REMOVING SESSION"); HttpSession session = request.getSession(); session.removeAttribute( "sessionObject" ); session.invalidate(); mailbox.destroy(); sessionObject.mailbox = null; sessionObject.mailCache = null; Debug.debug(Debug.DEBUG, "NOT CONNECTED, BOO"); } } } } } /** * * @param sessionObject * @param request */ private static void processLogout( SessionObject sessionObject, RequestWrapper request, boolean isPOST ) { if( buttonPressed( request, LOGOUT ) && isPOST) { Debug.debug(Debug.DEBUG, "LOGOUT, REMOVING SESSION"); HttpSession session = request.getSession(); session.removeAttribute( "sessionObject" ); session.invalidate(); POP3MailBox mailbox = sessionObject.mailbox; if (mailbox != null) { mailbox.destroy(); sessionObject.mailbox = null; sessionObject.mailCache = null; } sessionObject.info += _t("User logged out.") + '\n'; sessionObject.state = STATE_AUTH; } else if( sessionObject.mailbox == null && !buttonPressed(request, CANCEL)) { sessionObject.error += _t("Internal error, lost connection.") + '\n'; sessionObject.state = STATE_AUTH; } } /** * Process all buttons, which possibly change internal state. * Also processes ?show=x for a GET * * @param sessionObject * @param request * @param isPOST disallow button pushes if false */ private static void processStateChangeButtons(SessionObject sessionObject, RequestWrapper request, boolean isPOST ) { /* * LOGIN/LOGOUT */ if( sessionObject.state == STATE_AUTH && isPOST ) processLogin( sessionObject, request ); if( sessionObject.state != STATE_AUTH ) processLogout( sessionObject, request, isPOST ); /* * compose dialog */ if( sessionObject.state == STATE_NEW && isPOST ) { // We have to make sure to get the state right even if // the user hit the back button previously if( buttonPressed( request, SEND ) ) { if( sendMail( sessionObject, request ) ) sessionObject.state = STATE_LIST; } else if (buttonPressed( request, CANCEL ) || buttonPressed( request, SHOW ) || // A param, not a button, but we could be lost buttonPressed( request, PREVPAGE ) || // All these buttons are not shown but we could be lost buttonPressed( request, NEXTPAGE ) || buttonPressed( request, FIRSTPAGE ) || buttonPressed( request, LASTPAGE ) || buttonPressed( request, SETPAGESIZE ) || buttonPressed( request, MARKALL ) || buttonPressed( request, CLEAR ) || buttonPressed( request, INVERT ) || buttonPressed( request, SORT_ID ) || buttonPressed( request, SORT_SENDER ) || buttonPressed( request, SORT_SUBJECT ) || buttonPressed( request, SORT_DATE ) || buttonPressed( request, SORT_SIZE ) || buttonPressed( request, REFRESH ) || buttonPressed( request, LIST )) { sessionObject.state = STATE_LIST; sessionObject.sentMail = null; if( sessionObject.attachments != null ) sessionObject.attachments.clear(); } else if (buttonPressed( request, PREV ) || // All these buttons are not shown but we could be lost buttonPressed( request, NEXT ) || buttonPressed( request, DELETE )) { sessionObject.state = STATE_SHOW; sessionObject.sentMail = null; if( sessionObject.attachments != null ) sessionObject.attachments.clear(); } } /* * message dialog or config */ if((sessionObject.state == STATE_SHOW || sessionObject.state == STATE_CONFIG) && isPOST ) { if( buttonPressed( request, LIST ) ) { sessionObject.state = STATE_LIST; } else if (buttonPressed( request, CANCEL ) || buttonPressed( request, PREVPAGE ) || // All these buttons are not shown but we could be lost buttonPressed( request, NEXTPAGE ) || buttonPressed( request, FIRSTPAGE ) || buttonPressed( request, LASTPAGE ) || buttonPressed( request, SETPAGESIZE ) || buttonPressed( request, MARKALL ) || buttonPressed( request, CLEAR ) || buttonPressed( request, INVERT ) || buttonPressed( request, SORT_ID ) || buttonPressed( request, SORT_SENDER ) || buttonPressed( request, SORT_SUBJECT ) || buttonPressed( request, SORT_DATE ) || buttonPressed( request, SORT_SIZE ) || buttonPressed( request, REFRESH )) { sessionObject.state = STATE_LIST; } } /* * config */ if (sessionObject.state == STATE_CONFIG && isPOST) { if (buttonPressed(request, OFFLINE)) { // lost sessionObject.state = STATE_AUTH; } else if (buttonPressed(request, LOGIN)) { // lost sessionObject.state = STATE_AUTH; } } /* * buttons on both folder and message dialog */ if( sessionObject.state == STATE_SHOW || sessionObject.state == STATE_LIST ) { if( isPOST && buttonPressed( request, NEW ) ) { sessionObject.state = STATE_NEW; } boolean reply = false; boolean replyAll = false; boolean forward = false; sessionObject.replyTo = null; sessionObject.replyCC = null; sessionObject.body = null; sessionObject.subject = null; if( buttonPressed( request, REPLY ) ) reply = true; if( buttonPressed( request, REPLYALL ) ) { replyAll = true; } if( buttonPressed( request, FORWARD ) ) { forward = true; } if( reply || replyAll || forward ) { /* * try to find message */ String uidl = null; if( sessionObject.state == STATE_LIST ) { // these buttons are now hidden on the folder page, // but the idea is to use the first checked message List<Integer> items = getCheckedItems(request); if (!items.isEmpty()) { int pos = items.get(0).intValue(); uidl = sessionObject.folder.getElementAtPosXonCurrentPage( pos ); } } else { uidl = sessionObject.showUIDL; } if( uidl != null ) { Mail mail = sessionObject.mailCache.getMail( uidl, MailCache.FetchMode.ALL ); /* * extract original sender from Reply-To: or From: */ MailPart part = mail != null ? mail.getPart() : null; if (part != null) { if( reply || replyAll ) { if( mail.reply != null && Mail.validateAddress( mail.reply ) ) sessionObject.replyTo = mail.reply; else if( mail.sender != null && Mail.validateAddress( mail.sender ) ) sessionObject.replyTo = mail.sender; sessionObject.subject = _t("Re:") + ' ' + mail.formattedSubject; StringWriter text = new StringWriter(); PrintWriter pw = new PrintWriter( text ); pw.println( _t("On {0} {1} wrote:", mail.formattedDate + " UTC", sessionObject.replyTo) ); StringWriter text2 = new StringWriter(); PrintWriter pw2 = new PrintWriter( text2 ); showPart( pw2, part, 0, TEXT_ONLY ); pw2.flush(); String[] lines = DataHelper.split(text2.toString(), "\r\n"); for( int i = 0; i < lines.length; i++ ) pw.println( "> " + lines[i] ); pw.flush(); sessionObject.body = text.toString(); } if( replyAll ) { /* * extract additional recipients */ StringBuilder buf = new StringBuilder(); String pad = ""; if( mail.to != null ) { for( int i = 0; i < mail.to.length; i++ ) { buf.append( pad ); buf.append(mail.to[i]); pad = ", "; } } if( mail.cc != null ) { for( int i = 0; i < mail.cc.length; i++ ) { buf.append( pad ); buf.append(mail.cc[i]); pad = ", "; } } if( buf.length() > 0 ) sessionObject.replyCC = buf.toString(); } if( forward ) { sessionObject.subject = _t("Fwd:") + ' ' + mail.formattedSubject; String sender = null; if( mail.reply != null && Mail.validateAddress( mail.reply ) ) sender = Mail.getAddress( mail.reply ); else if( mail.sender != null && Mail.validateAddress( mail.sender ) ) sender = Mail.getAddress( mail.sender ); StringWriter text = new StringWriter(); PrintWriter pw = new PrintWriter( text ); pw.println(); pw.println(); pw.println(); pw.println( "---- " + _t("begin forwarded mail") + " ----" ); pw.println( "From: " + sender ); if( mail.to != null ) { String pad = "To: "; for( int i = 0; i < mail.to.length; i++ ) { pw.println( pad ); pw.println(mail.to[i]); pad = " "; } } if( mail.cc != null ) { String pad = "Cc: "; for( int i = 0; i < mail.cc.length; i++ ) { pw.println( pad ); pw.println(mail.cc[i]); pad = " "; } } if( mail.dateString != null ) pw.print( "Date: " + mail.dateString ); pw.println(); showPart( pw, part, 0, TEXT_ONLY ); pw.println( "---- " + _t("end forwarded mail") + " ----" ); pw.flush(); sessionObject.body = text.toString(); } sessionObject.state = STATE_NEW; } else { sessionObject.error += _t("Could not fetch mail body.") + '\n'; } } } } /* * folder view * SHOW is the one parameter that's a link, not a button, so we allow it for GET */ if( sessionObject.state == STATE_LIST || sessionObject.state == STATE_SHOW) { /* * check if user wants to view a message */ String show = request.getParameter( SHOW ); if( show != null && show.length() > 0 ) { try { int id = Integer.parseInt( show ); if( id >= 0 && id < sessionObject.folder.getPageSize() ) { String uidl = sessionObject.folder.getElementAtPosXonCurrentPage( id ); if( uidl != null ) { sessionObject.state = STATE_SHOW; sessionObject.showUIDL = uidl; } } } catch( NumberFormatException nfe ) { sessionObject.error += _t("Message id not valid.") + '\n'; } } } } /** * Returns e.g. 3,5 for ?check3=1&check5=1 (or POST equivalent) * @param request * @return non-null */ private static List<Integer> getCheckedItems(RequestWrapper request) { List<Integer> rv = new ArrayList<Integer>(8); for( Enumeration<String> e = request.getParameterNames(); e.hasMoreElements(); ) { String parameter = e.nextElement(); if( parameter.startsWith( "check" ) && request.getParameter( parameter ).equals("1")) { String number = parameter.substring( 5 ); try { rv.add(Integer.valueOf(number)); } catch( NumberFormatException nfe ) {} } } return rv; } /** * @param sessionObject * @param request */ private static void processGenericButtons(SessionObject sessionObject, RequestWrapper request) { // these two buttons are only on the folder view now if( buttonPressed( request, RELOAD ) ) { Config.reloadConfiguration(); int oldPageSize = sessionObject.folder.getPageSize(); int pageSize = Config.getProperty( Folder.PAGESIZE, Folder.DEFAULT_PAGESIZE ); if( pageSize != oldPageSize ) sessionObject.folder.setPageSize( pageSize ); sessionObject.info = _t("Configuration reloaded"); } if( buttonPressed( request, REFRESH ) ) { POP3MailBox mailbox = sessionObject.mailbox; if (mailbox == null) { sessionObject.error += _t("Internal error, lost connection.") + '\n'; sessionObject.state = STATE_AUTH; return; } // TODO how to do a "No new mail" message? mailbox.refresh(); sessionObject.error += mailbox.lastError(); sessionObject.mailCache.getMail(MailCache.FetchMode.HEADER); // get through cache so we have the disk-only ones too String[] uidls = sessionObject.mailCache.getUIDLs(); if (uidls != null) sessionObject.folder.setElements(uidls); sessionObject.pageChanged = true; } } /** * process buttons of compose message dialog * This must be called BEFORE processStateChangeButtons so we can add the attachment before SEND * * @param sessionObject * @param request */ private static void processComposeButtons(SessionObject sessionObject, RequestWrapper request) { String filename = request.getFilename( NEW_FILENAME ); // We handle an attachment whether sending or uploading if (filename != null && (buttonPressed(request, NEW_UPLOAD) || buttonPressed(request, SEND))) { Debug.debug(Debug.DEBUG, "Got filename in compose form: " + filename); int i = filename.lastIndexOf('/'); if( i != - 1 ) filename = filename.substring( i + 1 ); i = filename.lastIndexOf('\\'); if( i != -1 ) filename = filename.substring( i + 1 ); if( filename != null && filename.length() > 0 ) { InputStream in = request.getInputStream( NEW_FILENAME ); int l; try { l = in.available(); if( l > 0 ) { byte buf[] = new byte[l]; in.read( buf ); String contentType = request.getContentType( NEW_FILENAME ); Encoding encoding; String encodeTo; if( contentType.toLowerCase(Locale.US).startsWith( "text/" ) ) encodeTo = "quoted-printable"; else encodeTo = "base64"; encoding = EncodingFactory.getEncoding( encodeTo ); try { if( encoding != null ) { String data = encoding.encode(buf); if( sessionObject.attachments == null ) sessionObject.attachments = new ArrayList<Attachment>(); sessionObject.attachments.add( new Attachment(filename, contentType, encodeTo, data) ); } else { sessionObject.error += _t("No Encoding found for {0}", encodeTo) + '\n'; } } catch (EncodingException e1) { sessionObject.error += _t("Could not encode data: {0}", e1.getMessage()); } } } catch (IOException e) { sessionObject.error += _t("Error reading uploaded file: {0}", e.getMessage()) + '\n'; } } } else if( sessionObject.attachments != null && buttonPressed( request, DELETE_ATTACHMENT ) ) { for (Integer item : getCheckedItems(request)) { int n = item.intValue(); for( int i = 0; i < sessionObject.attachments.size(); i++ ) { Attachment attachment = sessionObject.attachments.get(i); if( attachment.hashCode() == n ) { sessionObject.attachments.remove( i ); break; } } } } } /** * process buttons of message view * @param sessionObject * @param request */ private static void processMessageButtons(SessionObject sessionObject, RequestWrapper request) { if( buttonPressed( request, PREV ) ) { String uidl = sessionObject.folder.getPreviousElement( sessionObject.showUIDL ); if( uidl != null ) sessionObject.showUIDL = uidl; } if( buttonPressed( request, NEXT ) ) { String uidl = sessionObject.folder.getNextElement( sessionObject.showUIDL ); if( uidl != null ) sessionObject.showUIDL = uidl; } sessionObject.reallyDelete = buttonPressed( request, DELETE ); if( buttonPressed( request, REALLYDELETE ) ) { /* * first find the next message */ String nextUIDL = sessionObject.folder.getNextElement( sessionObject.showUIDL ); if( nextUIDL == null ) { /* * nothing found? then look for the previous one */ nextUIDL = sessionObject.folder.getPreviousElement( sessionObject.showUIDL ); if( nextUIDL == null ) /* * still nothing found? then this was the last message, so go back to the folder */ sessionObject.state = STATE_LIST; } sessionObject.mailCache.delete( sessionObject.showUIDL ); sessionObject.folder.removeElement(sessionObject.showUIDL); sessionObject.showUIDL = nextUIDL; } } /** * process download link in message view * @param sessionObject * @param request * @return If true, we sent an attachment or 404, do not send any other response */ private static boolean processDownloadLink(SessionObject sessionObject, RequestWrapper request, HttpServletResponse response) { String str = request.getParameter(DOWNLOAD); boolean isRaw = false; if (str == null) { str = request.getParameter(RAW_ATTACHMENT); isRaw = str != null; } if( str != null ) { try { int hashCode = Integer.parseInt( str ); Mail mail = sessionObject.mailCache.getMail( sessionObject.showUIDL, MailCache.FetchMode.ALL ); MailPart part = mail != null ? getMailPartFromHashCode( mail.getPart(), hashCode ) : null; if( part != null ) { if (sendAttachment(sessionObject, part, response, isRaw)) return true; } } catch( NumberFormatException nfe ) {} // error if we get here sessionObject.error += _t("Attachment not found."); if (isRaw) { try { response.sendError(404, _t("Attachment not found.")); } catch (IOException ioe) {} } } return isRaw; } /** * Process save-as link in message view * * @param sessionObject * @param request * @return If true, we sent the file or 404, do not send any other response * @since 0.9.18 */ private static boolean processSaveAsLink(SessionObject sessionObject, RequestWrapper request, HttpServletResponse response) { String str = request.getParameter(SAVE_AS); if( str == null ) return false; Mail mail = sessionObject.mailCache.getMail( sessionObject.showUIDL, MailCache.FetchMode.ALL ); if( mail != null ) { if (sendMailSaveAs(sessionObject, mail, response)) return true; } // error if we get here sessionObject.error += _t("Message not found."); try { response.sendError(404, _t("Message not found.")); } catch (IOException ioe) {} return true; } /** * @param hashCode * @return the part or null */ private static MailPart getMailPartFromHashCode( MailPart part, int hashCode ) { if( part == null ) return null; if( part.hashCode() == hashCode ) return part; if( part.multipart || part.message ) { for( MailPart p : part.parts ) { MailPart subPart = getMailPartFromHashCode( p, hashCode ); if( subPart != null ) return subPart; } } return null; } /** * process buttons of folder view * @param sessionObject * @param request */ private static void processFolderButtons(SessionObject sessionObject, RequestWrapper request) { /* * process paging buttons */ if (buttonPressed(request, SETPAGESIZE)) { try { int pageSize = Math.max(5, Integer.parseInt(request.getParameter(PAGESIZE))); int oldPageSize = sessionObject.folder.getPageSize(); if( pageSize != oldPageSize ) sessionObject.folder.setPageSize( pageSize ); } catch( NumberFormatException nfe ) { sessionObject.error += _t("Invalid pagesize number, resetting to default value.") + '\n'; } } if( buttonPressed( request, PREVPAGE ) ) { sessionObject.pageChanged = true; sessionObject.folder.previousPage(); } else if( buttonPressed( request, NEXTPAGE ) ) { sessionObject.pageChanged = true; sessionObject.folder.nextPage(); } else if( buttonPressed( request, FIRSTPAGE ) ) { sessionObject.pageChanged = true; sessionObject.folder.firstPage(); } else if( buttonPressed( request, LASTPAGE ) ) { sessionObject.pageChanged = true; sessionObject.folder.lastPage(); } else if( buttonPressed( request, DELETE ) ) { int m = getCheckedItems(request).size(); if (m > 0) sessionObject.reallyDelete = true; else sessionObject.error += _t("No messages marked for deletion.") + '\n'; } else { if( buttonPressed( request, REALLYDELETE ) ) { List<String> toDelete = new ArrayList<String>(); for (Integer item : getCheckedItems(request)) { int n = item.intValue(); String uidl = sessionObject.folder.getElementAtPosXonCurrentPage( n ); if( uidl != null ) toDelete.add(uidl); } int numberDeleted = toDelete.size(); if (numberDeleted > 0) { sessionObject.mailCache.delete(toDelete); sessionObject.folder.removeElements(toDelete); sessionObject.pageChanged = true; sessionObject.info += ngettext("1 message deleted.", "{0} messages deleted.", numberDeleted); //sessionObject.error += _t("Error deleting message: {0}", sessionObject.mailbox.lastError()) + '\n'; } } sessionObject.reallyDelete = false; } sessionObject.markAll = buttonPressed( request, MARKALL ); sessionObject.clear = buttonPressed( request, CLEAR ); sessionObject.invert = buttonPressed( request, INVERT ); } /* * process sorting buttons */ private static void processSortingButtons(SessionObject sessionObject, RequestWrapper request) { //processSortingButton( sessionObject, request, SORT_ID ); processSortingButton( sessionObject, request, SORT_SENDER ); processSortingButton( sessionObject, request, SORT_SUBJECT ); processSortingButton( sessionObject, request, SORT_DATE ); processSortingButton( sessionObject, request, SORT_SIZE ); } /** * @param sessionObject * @param request * @param sort_id */ private static void processSortingButton(SessionObject sessionObject, RequestWrapper request, String sort_id ) { String str = request.getParameter( sort_id ); if( str != null ) { if( str.equalsIgnoreCase("up")) { sessionObject.folder.setSortingDirection(Folder.SortOrder.UP); sessionObject.folder.sortBy( sort_id ); } else if( str.equalsIgnoreCase("down")) { sessionObject.folder.setSortingDirection(Folder.SortOrder.DOWN); sessionObject.folder.sortBy( sort_id ); } } } /* * process config buttons, both entering and exiting */ private static void processConfigButtons(SessionObject sessionObject, RequestWrapper request) { if (buttonPressed(request, SAVE)) { try { String raw = request.getParameter(CONFIG_TEXT); if (raw == null) return; Properties props = new Properties(); DataHelper.loadProps(props, new ByteArrayInputStream(DataHelper.getUTF8(raw))); // for safety, disallow changing host via UI String oldHost = Config.getProperty(CONFIG_HOST, DEFAULT_HOST); String newHost = props.getProperty(CONFIG_HOST); if (newHost == null) { props.setProperty(CONFIG_HOST, oldHost); } else if (!newHost.equals(oldHost) && !newHost.equals("localhost")) { props.setProperty(CONFIG_HOST, oldHost); File cfg = new File(I2PAppContext.getGlobalContext().getConfigDir(), "susimail.config"); sessionObject.error += _t("Host unchanged. Edit configation file {0} to change host.", cfg.getAbsolutePath()) + '\n'; } Config.saveConfiguration(props); String ps = props.getProperty(Folder.PAGESIZE); if (sessionObject.folder != null && ps != null) { try { int pageSize = Math.max(5, Integer.parseInt(request.getParameter(PAGESIZE))); int oldPageSize = sessionObject.folder.getPageSize(); if( pageSize != oldPageSize ) sessionObject.folder.setPageSize( pageSize ); } catch( NumberFormatException nfe ) {} } boolean release = !Boolean.parseBoolean(props.getProperty(CONFIG_DEBUG)); Debug.setLevel( release ? Debug.ERROR : Debug.DEBUG ); sessionObject.state = sessionObject.folder != null ? STATE_LIST : STATE_AUTH; sessionObject.info = _t("Configuration saved"); } catch (IOException ioe) { sessionObject.error = ioe.toString(); } } else if (buttonPressed(request, SETPAGESIZE)) { try { int pageSize = Math.max(5, Integer.parseInt(request.getParameter(PAGESIZE))); Properties props = Config.getProperties(); props.setProperty(Folder.PAGESIZE, String.valueOf(pageSize)); Config.saveConfiguration(props); if (sessionObject.folder != null) { int oldPageSize = sessionObject.folder.getPageSize(); if( pageSize != oldPageSize ) sessionObject.folder.setPageSize( pageSize ); sessionObject.state = STATE_LIST; } else { sessionObject.state = STATE_AUTH; } } catch (IOException ioe) { sessionObject.error = ioe.toString(); } catch( NumberFormatException nfe ) { sessionObject.error += _t("Invalid pagesize number, resetting to default value.") + '\n'; } } else if (buttonPressed(request, CANCEL)) { sessionObject.state = (sessionObject.folder != null) ? STATE_LIST : STATE_AUTH; } else if (buttonPressed(request, CONFIGURE)) { sessionObject.state = STATE_CONFIG; } } /** * @param httpSession * @return non-null */ private synchronized SessionObject getSessionObject( HttpSession httpSession ) { SessionObject sessionObject = (SessionObject)httpSession.getAttribute( "sessionObject" ); if( sessionObject == null ) { sessionObject = new SessionObject(); httpSession.setAttribute( "sessionObject", sessionObject ); Debug.debug(Debug.DEBUG, "NEW session " + httpSession.getId() + " state = " + sessionObject.state); } else { Debug.debug(Debug.DEBUG, "Existing session " + httpSession.getId() + " state = " + sessionObject.state + " created " + new Date(httpSession.getCreationTime())); } return sessionObject; } /** * Copied from net.i2p.router.web.CSSHelper * @param ua null ok * @since 0.9.7 */ private static boolean isMobile(String ua) { if (ua == null) return false; return // text (ua.startsWith("Lynx") || ua.startsWith("w3m") || ua.startsWith("ELinks") || ua.startsWith("Links") || ua.startsWith("Dillo") || ua.startsWith("Emacs-w3m") || // mobile // http://www.zytrax.com/tech/web/mobile_ids.html // Android tablet UAs don't have "Mobile" in them (ua.contains("Android") && ua.contains("Mobile")) || ua.contains("iPhone") || ua.contains("iPod") || ua.contains("iPad") || ua.contains("Kindle") || ua.contains("Mobile") || ua.contains("Nintendo Wii") || ua.contains("Opera Mini") || ua.contains("Opera Mobi") || ua.contains("Palm") || ua.contains("PLAYSTATION") || ua.contains("Playstation") || ua.contains("Profile/MIDP-") || ua.contains("SymbianOS") || ua.contains("Windows CE") || ua.contains("Windows Phone") || ua.startsWith("BlackBerry") || ua.startsWith("DoCoMo") || ua.startsWith("Nokia") || ua.startsWith("OPWV-SDK") || ua.startsWith("MOT-") || ua.startsWith("SAMSUNG-") || ua.startsWith("nook") || ua.startsWith("SCH-") || ua.startsWith("SEC-") || ua.startsWith("SonyEricsson") || ua.startsWith("Vodafone")); } /** * The entry point for all web page loads * * @param httpRequest * @param response * @param isPOST disallow button pushes if false * @throws IOException * @throws ServletException */ private void processRequest( HttpServletRequest httpRequest, HttpServletResponse response, boolean isPOST ) throws IOException, ServletException { String theme = Config.getProperty(CONFIG_THEME, DEFAULT_THEME); I2PAppContext ctx = I2PAppContext.getGlobalContext(); boolean universalTheming = ctx.getBooleanProperty(RC_PROP_UNIVERSAL_THEMING); if (universalTheming) { // Fetch routerconsole theme (or use our default if it doesn't exist) theme = ctx.getProperty(RC_PROP_THEME, DEFAULT_THEME); // Ensure that theme exists String[] themes = getThemes(); boolean themeExists = false; for (int i = 0; i < themes.length; i++) { if (themes[i].equals(theme)) themeExists = true; } if (!themeExists) { theme = DEFAULT_THEME; } } boolean forceMobileConsole = ctx.getBooleanProperty(RC_PROP_FORCE_MOBILE_CONSOLE); boolean isMobile = (forceMobileConsole || isMobile(httpRequest.getHeader("User-Agent"))); httpRequest.setCharacterEncoding("UTF-8"); response.setCharacterEncoding("UTF-8"); response.setHeader("X-Frame-Options", "SAMEORIGIN"); response.setHeader("Content-Security-Policy", "default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'"); response.setHeader("X-XSS-Protection", "1; mode=block"); response.setHeader("X-Content-Type-Options", "nosniff"); response.setHeader("Referrer-Policy", "no-referrer"); RequestWrapper request = new RequestWrapper( httpRequest ); SessionObject sessionObject = null; String subtitle = ""; HttpSession httpSession = request.getSession( true ); sessionObject = getSessionObject( httpSession ); synchronized( sessionObject ) { sessionObject.error = ""; sessionObject.info = ""; sessionObject.pageChanged = false; sessionObject.themePath = "/themes/susimail/" + theme + '/'; sessionObject.imgPath = sessionObject.themePath + "images/"; sessionObject.isMobile = isMobile; if (isPOST) { String nonce = request.getParameter(SUSI_NONCE); if (nonce == null || !sessionObject.isValidNonce(nonce)) { // These two strings are already in the router console FormHandler, // so translate with that bundle. sessionObject.error = consoleGetString( "Invalid form submission, probably because you used the 'back' or 'reload' button on your browser. Please resubmit.", ctx) + '\n' + consoleGetString("If the problem persists, verify that you have cookies enabled in your browser.", ctx); isPOST = false; } } // This must be called to add the attachment before // processStateChangeButtons() sends the message if(isPOST && sessionObject.state == STATE_NEW) processComposeButtons( sessionObject, request ); int oldState = sessionObject.state; processStateChangeButtons( sessionObject, request, isPOST ); if (isPOST) processConfigButtons( sessionObject, request ); int newState = sessionObject.state; if (oldState != newState) Debug.debug(Debug.DEBUG, "STATE CHANGE from " + oldState + " to " + newState); // Set in web.xml //if (oldState == STATE_AUTH && newState != STATE_AUTH) { // int oldIdle = httpSession.getMaxInactiveInterval(); // httpSession.setMaxInactiveInterval(60*60*24); // seconds // int newIdle = httpSession.getMaxInactiveInterval(); // Debug.debug(Debug.DEBUG, "Changed idle from " + oldIdle + " to " + newIdle); //} if( sessionObject.state != STATE_AUTH ) { if (isPOST) processGenericButtons( sessionObject, request ); } if( sessionObject.state == STATE_LIST ) { if (isPOST) processFolderButtons( sessionObject, request ); processSortingButtons( sessionObject, request ); for( Iterator<String> it = sessionObject.folder.currentPageIterator(); it != null && it.hasNext(); ) { String uidl = it.next(); Mail mail = sessionObject.mailCache.getMail( uidl, MailCache.FetchMode.HEADER ); if( mail != null && mail.error.length() > 0 ) { sessionObject.error += mail.error; mail.error = ""; } } } if( sessionObject.state == STATE_SHOW ) { if (isPOST) processMessageButtons( sessionObject, request ); // ?download=nnn link should be valid in any state // but depends on current UIDL if (processDownloadLink(sessionObject, request, response)) { // download or raw view sent, or 404 return; } if (isPOST && processSaveAsLink(sessionObject, request, response)) { // download or sent, or 404 return; } // If the last message has just been deleted then // sessionObject.state = STATE_LIST and // sessionObject.showUIDL = null if ( sessionObject.showUIDL != null ) { Mail mail = sessionObject.mailCache.getMail( sessionObject.showUIDL, MailCache.FetchMode.ALL ); if( mail != null && mail.error.length() > 0 ) { sessionObject.error += mail.error; mail.error = ""; } } } /* * update folder content */ if( sessionObject.state == STATE_LIST ) { // get through cache so we have the disk-only ones too String[] uidls = sessionObject.mailCache.getUIDLs(); if (uidls != null) { // TODO why every time? sessionObject.folder.setElements(uidls); } } PrintWriter out = response.getWriter(); /* * build subtitle */ if( sessionObject.state == STATE_AUTH ) subtitle = _t("Login"); else if( sessionObject.state == STATE_LIST ) { // mailbox.getNumMails() forces a connection, don't use it // Not only does it slow things down, but a failure causes all our messages to "vanish" //subtitle = ngettext("1 Message", "{0} Messages", sessionObject.mailbox.getNumMails()); subtitle = ngettext("1 Message", "{0} Messages", sessionObject.folder.getSize()); } else if( sessionObject.state == STATE_SHOW ) { Mail mail = sessionObject.mailCache.getMail(sessionObject.showUIDL, MailCache.FetchMode.HEADER); if (mail != null && mail.shortSubject != null) subtitle = mail.shortSubject; // already HTML encoded else subtitle = _t("Show Message"); } else if( sessionObject.state == STATE_NEW ) { subtitle = _t("New Message"); } else if( sessionObject.state == STATE_CONFIG ) { subtitle = _t("Configuration"); } response.setContentType( "text/html" ); /* * write header */ out.println( "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\">\n<html>\n" + "<head>\n" + "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">\n" + "<title>" + _t("SusiMail") + " - " + subtitle + "</title>\n" + "<link rel=\"stylesheet\" type=\"text/css\" href=\"" + sessionObject.themePath + "susimail.css?" + CoreVersion.VERSION + "\">\n" ); if (sessionObject.isMobile ) { out.println( "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, maximum-scale=2.0, user-scalable=yes\" />\n" + "<link rel=\"stylesheet\" type=\"text/css\" href=\"" + sessionObject.themePath + "mobile.css\" />\n" ); } if(sessionObject.state != STATE_AUTH) out.println("<link rel=\"stylesheet\" href=\"/susimail/css/print.css\" type=\"text/css\" media=\"print\" />"); if (sessionObject.state == STATE_NEW) { // TODO cancel if to and body are empty out.println( "<script type=\"text/javascript\">\n" + "window.onbeforeunload = function () {" + "return \"" + _t("Message has not been sent. Do you want to discard it?") + "\";" + "};\n" + "</script>" ); out.println("<script src=\"/susimail/js/compose.js\" type=\"text/javascript\"></script>"); } else if (sessionObject.state == STATE_LIST) { out.println("<script src=\"/susimail/js/folder.js\" type=\"text/javascript\"></script>"); } out.print("</head>\n<body" + (sessionObject.state == STATE_LIST ? " onload=\"deleteboxclicked()\">" : ">")); String nonce = sessionObject.state == STATE_AUTH ? LOGIN_NONCE : Long.toString(ctx.random().nextLong()); sessionObject.addNonce(nonce); out.println( "<div class=\"page\"><div class=\"header\"><img class=\"header\" src=\"" + sessionObject.imgPath + "susimail.png\" alt=\"Susimail\"></div>\n" + "<form method=\"POST\" enctype=\"multipart/form-data\" action=\"" + myself + "\" accept-charset=\"UTF-8\">\n" + "<input type=\"hidden\" name=\"" + SUSI_NONCE + "\" value=\"" + nonce + "\">"); if( sessionObject.error != null && sessionObject.error.length() > 0 ) { out.println( "<p class=\"error\">" + quoteHTML(sessionObject.error).replace("\n", "<br>") + "</p>" ); } if( sessionObject.info != null && sessionObject.info.length() > 0 ) { out.println( "<p class=\"info\"><b>" + quoteHTML(sessionObject.info).replace("\n", "<br>") + "</b></p>" ); } /* * now write body */ if( sessionObject.state == STATE_AUTH ) showLogin( out ); else if( sessionObject.state == STATE_LIST ) showFolder( out, sessionObject, request ); else if( sessionObject.state == STATE_SHOW ) showMessage( out, sessionObject ); else if( sessionObject.state == STATE_NEW ) showCompose( out, sessionObject, request ); else if( sessionObject.state == STATE_CONFIG ) showConfig(out, sessionObject); //out.println( "</form><div id=\"footer\"><hr><p class=\"footer\">susimail v0." + version +" " + ( RELEASE ? "release" : "development" ) + " © 2004-2005 <a href=\"mailto:susi23@mail.i2p\">susi</a></div></div></body>\n</html>"); out.println( "</form><div class=\"footer\"><hr><p class=\"footer\">susimail © 2004-2005 susi</p></div></div></body>\n</html>"); out.flush(); } } /** * Translate with the console bundle. * @since 0.9.27 */ private static String consoleGetString(String s, I2PAppContext ctx) { return Translate.getString(s, ctx, CONSOLE_BUNDLE_NAME); } /** * @param sessionObject * @param response * @param isRaw if true, don't zip it * @return success */ private static boolean sendAttachment(SessionObject sessionObject, MailPart part, HttpServletResponse response, boolean isRaw) { boolean shown = false; if(part != null) { ReadBuffer content = part.buffer; if( part.encoding != null ) { try { // why +2 ?? content = part.decode(2); } catch (DecodingException e) { sessionObject.error += _t("Error decoding content: {0}", e.getMessage()) + '\n'; content = null; } } if(content == null) return false; if (isRaw) { try { if (part.type != null) response.setContentType(part.type); response.setContentLength(content.length); // cache-control? response.getOutputStream().write(content.content, content.offset, content.length); shown = true; } catch (IOException e) { e.printStackTrace(); } } else { ZipOutputStream zip = null; try { zip = new ZipOutputStream( response.getOutputStream() ); String name; if( part.filename != null ) name = part.filename; else if( part.name != null ) name = part.name; else name = "part" + part.hashCode(); String name2 = sanitizeFilename(name); response.setContentType( "application/zip; name=\"" + name2 + ".zip\"" ); response.addHeader( "Content-Disposition", "attachment; filename=\"" + name2 + ".zip\"" ); ZipEntry entry = new ZipEntry( name ); zip.putNextEntry( entry ); zip.write( content.content, content.offset, content.length ); zip.closeEntry(); zip.finish(); shown = true; } catch (IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } finally { if ( zip != null) try { zip.close(); } catch (IOException ioe) {} } } } return shown; } /** * Send the mail to be saved by the browser * * @param sessionObject * @param response * @return success * @since 0.9.18 */ private static boolean sendMailSaveAs(SessionObject sessionObject, Mail mail, HttpServletResponse response) { ReadBuffer content = mail.getBody(); if(content == null) return false; String name = mail.subject != null ? sanitizeFilename(mail.subject) : "message"; try { response.setContentType("message/rfc822"); response.setContentLength(content.length); // cache-control? response.addHeader( "Content-Disposition", "attachment; filename=\"" + name + ".eml\"" ); response.getOutputStream().write(content.content, content.offset, content.length); return true; } catch (IOException e) { e.printStackTrace(); return false; } } /** * Convert the UTF-8 to ISO-8859-1 suitable for inclusion in a header. * This will result in a bunch of ??? for non-Western languages. * * @param sessionObject * @param response * @return success * @since 0.9.18 */ private static String sanitizeFilename(String name) { try { name = new String(name.getBytes("ISO-8859-1"), "ISO-8859-1"); } catch( UnsupportedEncodingException uee ) {} // strip control chars? name = name.replace('"', '_'); return name; } /** * @param sessionObject * @param request * @return success */ private static boolean sendMail( SessionObject sessionObject, RequestWrapper request ) { boolean ok = true; String from = request.getParameter( NEW_FROM ); String to = request.getParameter( NEW_TO ); String cc = request.getParameter( NEW_CC ); String bcc = request.getParameter( NEW_BCC ); String subject = request.getParameter( NEW_SUBJECT, _t("no subject") ); String text = request.getParameter( NEW_TEXT, "" ); boolean fixed = Boolean.parseBoolean(Config.getProperty( CONFIG_SENDER_FIXED, "true" )); if (fixed) { String domain = Config.getProperty( CONFIG_SENDER_DOMAIN, "mail.i2p" ); from = "<" + sessionObject.user + "@" + domain + ">"; } ArrayList<String> toList = new ArrayList<String>(); ArrayList<String> ccList = new ArrayList<String>(); ArrayList<String> bccList = new ArrayList<String>(); ArrayList<String> recipients = new ArrayList<String>(); String sender = null; if( from == null || !Mail.validateAddress( from ) ) { ok = false; sessionObject.error += _t("Found no valid sender address.") + '\n'; } else { sender = Mail.getAddress( from ); if( sender == null || sender.length() == 0 ) { ok = false; sessionObject.error += _t("Found no valid address in \\''{0}\\''.", quoteHTML( from )) + '\n'; } } ok = Mail.getRecipientsFromList( toList, to, ok ); ok = Mail.getRecipientsFromList( ccList, cc, ok ); ok = Mail.getRecipientsFromList( bccList, bcc, ok ); recipients.addAll( toList ); recipients.addAll( ccList ); recipients.addAll( bccList ); String bccToSelf = request.getParameter( NEW_BCC_TO_SELF ); boolean toSelf = "1".equals(bccToSelf); // save preference in session sessionObject.bccToSelf = toSelf; if (toSelf) recipients.add( sender ); if( toList.isEmpty() ) { ok = false; sessionObject.error += _t("No recipients found.") + '\n'; } Encoding qp = EncodingFactory.getEncoding( "quoted-printable" ); Encoding hl = EncodingFactory.getEncoding( "HEADERLINE" ); if( qp == null ) { ok = false; // can't happen, don't translate sessionObject.error += "Internal error: Quoted printable encoder not available."; } if( hl == null ) { ok = false; // can't happen, don't translate sessionObject.error += "Internal error: Header line encoder not available."; } if( ok ) { StringBuilder body = new StringBuilder(); body.append( "From: " + from + "\r\n" ); Mail.appendRecipients( body, toList, "To: " ); Mail.appendRecipients( body, ccList, "To: " ); body.append( "Subject: " ); try { body.append( hl.encode( subject ) ); } catch (EncodingException e) { ok = false; sessionObject.error += e.getMessage(); } String boundary = "_="+(int)(Math.random()*Integer.MAX_VALUE)+""+(int)(Math.random()*Integer.MAX_VALUE); boolean multipart = false; if( sessionObject.attachments != null && !sessionObject.attachments.isEmpty() ) { multipart = true; body.append( "\r\nMIME-Version: 1.0\r\nContent-type: multipart/mixed; boundary=\"" + boundary + "\"\r\n\r\n" ); } else { body.append( "\r\nMIME-Version: 1.0\r\nContent-type: text/plain; charset=\"utf-8\"\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\n" ); } try { if( multipart ) body.append( "--" + boundary + "\r\nContent-type: text/plain; charset=\"utf-8\"\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\n" ); body.append( qp.encode( text ) ); } catch (EncodingException e) { ok = false; sessionObject.error += e.getMessage(); } if( multipart ) { for( Attachment attachment : sessionObject.attachments ) { body.append( "\r\n--" + boundary + "\r\nContent-type: " + attachment.getContentType() + "\r\nContent-Disposition: attachment; filename=\"" + attachment.getFileName() + "\"\r\nContent-Transfer-Encoding: " + attachment.getTransferEncoding() + "\r\n\r\n" ); body.append( attachment.getData() ); } body.append( "\r\n--" + boundary + "--\r\n" ); } // TODO set to the StringBuilder instead so SMTP can replace() in place sessionObject.sentMail = body.toString(); if( ok ) { SMTPClient relay = new SMTPClient(); if( relay.sendMail( sessionObject.host, sessionObject.smtpPort, sessionObject.user, sessionObject.pass, sender, recipients.toArray(), sessionObject.sentMail ) ) { sessionObject.info += _t("Mail sent."); sessionObject.sentMail = null; if( sessionObject.attachments != null ) sessionObject.attachments.clear(); } else { ok = false; sessionObject.error += relay.error; } } } return ok; } /** * */ @Override public void doGet( HttpServletRequest request, HttpServletResponse response ) throws IOException, ServletException { processRequest( request, response, false ); } /** * */ @Override public void doPost( HttpServletRequest request, HttpServletResponse response ) throws IOException, ServletException { processRequest( request, response, true ); } /** * * @param out * @param sessionObject * @param request */ private static void showCompose( PrintWriter out, SessionObject sessionObject, RequestWrapper request ) { out.println("<div class=\"topbuttons\">"); out.println( button( SEND, _t("Send") ) + spacer + button( CANCEL, _t("Cancel") )); out.println("</div>"); //if (Config.hasConfigFile()) // out.println(button( RELOAD, _t("Reload Config") ) + spacer); //out.println(button( LOGOUT, _t("Logout") ) ); String from = request.getParameter( NEW_FROM ); boolean fixed = Boolean.parseBoolean(Config.getProperty( CONFIG_SENDER_FIXED, "true" )); if (from == null || !fixed) { String user = sessionObject.user; String name = Config.getProperty(CONFIG_SENDER_NAME); if (name != null) { name = name.trim(); if (name.contains(" ")) from = '"' + name + "\" "; else from = name + ' '; } else { from = ""; } if (user.contains("@")) { from += '<' + user + '>'; } else { String domain = Config.getProperty( CONFIG_SENDER_DOMAIN, "mail.i2p" ); if (from.length() == 0) from = user + ' '; from += '<' + user + '@' + domain + '>'; } } String to = request.getParameter( NEW_TO, sessionObject.replyTo != null ? sessionObject.replyTo : "" ); String cc = request.getParameter( NEW_CC, sessionObject.replyCC != null ? sessionObject.replyCC : "" ); String bcc = request.getParameter( NEW_BCC, "" ); String subject = request.getParameter( NEW_SUBJECT, sessionObject.subject != null ? sessionObject.subject : "" ); String text = request.getParameter( NEW_TEXT, sessionObject.body != null ? sessionObject.body : "" ); sessionObject.replyTo = null; sessionObject.replyCC = null; sessionObject.subject = null; sessionObject.body = null; out.println( "<table cellspacing=\"0\" cellpadding=\"5\">\n" + "<tr><td colspan=\"2\" align=\"center\"><hr></td></tr>\n" + "<tr><td align=\"right\">" + _t("From") + ":</td><td align=\"left\"><input type=\"text\" size=\"80\" name=\"" + NEW_FROM + "\" value=\"" + quoteHTML(from) + "\" " + ( fixed ? "disabled" : "" ) +"></td></tr>\n" + "<tr><td align=\"right\">" + _t("To") + ":</td><td align=\"left\"><input type=\"text\" size=\"80\" name=\"" + NEW_TO + "\" value=\"" + quoteHTML(to) + "\"></td></tr>\n" + "<tr><td align=\"right\">" + _t("Cc") + ":</td><td align=\"left\"><input type=\"text\" size=\"80\" name=\"" + NEW_CC + "\" value=\"" + quoteHTML(cc) + "\"></td></tr>\n" + "<tr><td align=\"right\">" + _t("Bcc") + ":</td><td align=\"left\"><input type=\"text\" size=\"80\" name=\"" + NEW_BCC + "\" value=\"" + quoteHTML(bcc) + "\"></td></tr>\n" + "<tr><td align=\"right\">" + _t("Bcc to self") + ": </td><td align=\"left\"><input type=\"checkbox\" class=\"optbox\" name=\"" + NEW_BCC_TO_SELF + "\" value=\"1\" " + (sessionObject.bccToSelf ? "checked" : "" ) + "></td></tr>\n" + "<tr><td align=\"right\">" + _t("Subject") + ":</td><td align=\"left\"><input type=\"text\" size=\"80\" name=\"" + NEW_SUBJECT + "\" value=\"" + quoteHTML(subject) + "\"></td></tr>\n" + "<tr><td colspan=\"2\" align=\"center\"><textarea cols=\"" + Config.getProperty( CONFIG_COMPOSER_COLS, 80 )+ "\" rows=\"" + Config.getProperty( CONFIG_COMPOSER_ROWS, 10 )+ "\" name=\"" + NEW_TEXT + "\">" + text + "</textarea>" + "<tr class=\"bottombuttons\"><td colspan=\"2\" align=\"center\"><hr></td></tr>\n" + "<tr class=\"bottombuttons\"><td align=\"right\">" + _t("Add Attachment") + ":</td><td align=\"left\"><input type=\"file\" size=\"50%\" name=\"" + NEW_FILENAME + "\" value=\"\"></td></tr>" + // TODO disable/hide in JS if no file selected "<tr class=\"bottombuttons\"><td> </td><td align=\"left\">" + button(NEW_UPLOAD, _t("Add another attachment")) + "</td></tr>"); if( sessionObject.attachments != null && !sessionObject.attachments.isEmpty() ) { boolean wroteHeader = false; for( Attachment attachment : sessionObject.attachments ) { if( !wroteHeader ) { out.println("<tr><td align=\"right\">" + _t("Attachments") + ":</td>"); wroteHeader = true; } else { out.println("<tr><td align=\"right\"> </td>"); } out.println("<td align=\"left\"><input type=\"checkbox\" class=\"optbox\" name=\"check" + attachment.hashCode() + "\" value=\"1\"> " + quoteHTML(attachment.getFileName()) + "</td></tr>"); } // TODO disable in JS if none selected out.println("<tr class=\"bottombuttons\"><td> </td><td align=\"left\">" + button( DELETE_ATTACHMENT, _t("Delete selected attachments") ) + "</td></tr>"); } out.println( "</table>" ); } /** * * @param out */ private static void showLogin( PrintWriter out ) { boolean fixed = Boolean.parseBoolean(Config.getProperty( CONFIG_PORTS_FIXED, "true" )); String host = Config.getProperty( CONFIG_HOST, DEFAULT_HOST ); String pop3 = Config.getProperty( CONFIG_PORTS_POP3, "" + DEFAULT_POP3PORT ); String smtp = Config.getProperty( CONFIG_PORTS_SMTP, "" + DEFAULT_SMTPPORT ); out.println( "<table cellspacing=\"3\" cellpadding=\"5\">\n" + // current postman hq length limits 16/12, new postman version 32/32 "<tr><td align=\"right\" width=\"30%\">" + _t("User") + "</td><td width=\"40%\" align=\"left\"><input type=\"text\" size=\"32\" name=\"" + USER + "\" value=\"" + "\"> @mail.i2p</td></tr>\n" + "<tr><td align=\"right\" width=\"30%\">" + _t("Password") + "</td><td width=\"40%\" align=\"left\"><input type=\"password\" size=\"32\" name=\"pass\" value=\"" + "\"></td></tr>\n"); // which is better? //if (!fixed) { if (true) { out.println( "<tr><td align=\"right\" width=\"30%\">" + _t("Host") + "</td><td width=\"40%\" align=\"left\"><input type=\"text\" size=\"32\" name=\"" + HOST +"\" value=\"" + quoteHTML(host) + "\"" + ( fixed ? " disabled" : "" ) + "></td></tr>\n" + "<tr><td align=\"right\" width=\"30%\">" + _t("POP3 Port") + "</td><td width=\"40%\" align=\"left\"><input type=\"text\" style=\"text-align: right;\" size=\"5\" name=\"" + POP3 +"\" value=\"" + quoteHTML(pop3) + "\"" + ( fixed ? " disabled" : "" ) + "></td></tr>\n" + "<tr><td align=\"right\" width=\"30%\">" + _t("SMTP Port") + "</td><td width=\"40%\" align=\"left\"><input type=\"text\" style=\"text-align: right;\" size=\"5\" name=\"" + SMTP +"\" value=\"" + quoteHTML(smtp) + "\"" + ( fixed ? " disabled" : "" ) + "></td></tr>\n"); } out.println( "<tr><td colspan=\"2\"> </td></tr>\n" + "<tr><td></td><td align=\"left\">" + button( LOGIN, _t("Login") ) + spacer + button(OFFLINE, _t("Read Mail Offline") ) + //spacer + //" <input class=\"cancel\" type=\"reset\" value=\"" + _t("Reset") + "\">" + spacer + button(CONFIGURE, _t("Settings")) + "</td></tr>\n" + "<tr><td colspan=\"2\"> </td></tr>\n" + "<tr><td></td><td align=\"left\"><a href=\"http://hq.postman.i2p/?page_id=14\">" + _t("Learn about I2P mail") + "</a></td></tr>\n" + "<tr><td></td><td align=\"left\"><a href=\"http://hq.postman.i2p/?page_id=16\">" + _t("Create Account") + "</a></td></tr>\n" + "</table>"); } /** * * @param out * @param sessionObject * @param request */ private static void showFolder( PrintWriter out, SessionObject sessionObject, RequestWrapper request ) { out.println("<div class=\"topbuttons\">"); out.println( button( NEW, _t("New") ) + spacer); // In theory, these are valid and will apply to the first checked message, // but that's not obvious and did it work? //button( REPLY, _t("Reply") ) + //button( REPLYALL, _t("Reply All") ) + //button( FORWARD, _t("Forward") ) + spacer + //button( DELETE, _t("Delete") ) + spacer + out.println(button( REFRESH, _t("Check Mail") ) + spacer); //if (Config.hasConfigFile()) // out.println(button( RELOAD, _t("Reload Config") ) + spacer); out.println(button( LOGOUT, _t("Logout") )); if (sessionObject.folder.getPages() > 1) showPageButtons(out, sessionObject.folder); out.println("</div>"); String curSort = sessionObject.folder.getCurrentSortBy(); Folder.SortOrder curOrder = sessionObject.folder.getCurrentSortingDirection(); out.println("<table id=\"mailbox\" cellspacing=\"0\" cellpadding=\"5\">\n" + "<tr><td colspan=\"9\"><hr></td></tr>\n<tr>" + thSpacer + thSpacer + "<th>" + sortHeader( SORT_SENDER, _t("From"), sessionObject.imgPath, curSort, curOrder ) + "</th>" + thSpacer + "<th>" + sortHeader( SORT_SUBJECT, _t("Subject"), sessionObject.imgPath, curSort, curOrder ) + "</th>" + thSpacer + "<th>" + sortHeader( SORT_DATE, _t("Date"), sessionObject.imgPath, curSort, curOrder ) + //sortHeader( SORT_ID, "", sessionObject.imgPath ) + "</th>" + thSpacer + "<th>" + sortHeader( SORT_SIZE, _t("Size"), sessionObject.imgPath, curSort, curOrder ) + "</th></tr>" ); int bg = 0; int i = 0; for( Iterator<String> it = sessionObject.folder.currentPageIterator(); it != null && it.hasNext(); ) { String uidl = it.next(); Mail mail = sessionObject.mailCache.getMail( uidl, MailCache.FetchMode.HEADER ); if (mail == null) { i++; continue; } String type; if (mail.isSpam()) type = "linkspam"; else if (mail.isNew()) type = "linknew"; else type = "linkold"; String link = "<a href=\"" + myself + "?" + SHOW + "=" + i + "\" class=\"" + type + "\">"; String jslink = " onclick=\"document.location='" + myself + '?' + SHOW + '=' + i + "';\" "; boolean idChecked = false; String checkId = sessionObject.pageChanged ? null : request.getParameter( "check" + i ); if( checkId != null && checkId.equals("1")) idChecked = true; if( sessionObject.markAll ) idChecked = true; if( sessionObject.invert ) idChecked = !idChecked; if( sessionObject.clear ) idChecked = false; //Debug.debug( Debug.DEBUG, "check" + i + ": checkId=" + checkId + ", idChecked=" + idChecked + ", pageChanged=" + sessionObject.pageChanged + // ", markAll=" + sessionObject.markAll + // ", invert=" + sessionObject.invert + // ", clear=" + sessionObject.clear ); out.println( "<tr class=\"list" + bg + "\">" + "<td><input type=\"checkbox\" class=\"optbox\" name=\"check" + i + "\" value=\"1\"" + " onclick=\"deleteboxclicked();\" " + ( idChecked ? "checked" : "" ) + ">" + "</td><td " + jslink + ">" + (mail.isNew() ? "<img src=\"/susimail/icons/flag_green.png\" alt=\"\" title=\"" + _t("Message is new") + "\">" : " ") + "</td><td " + jslink + ">" + // mail.shortSender and mail.shortSubject already html encoded link + mail.shortSender + "</a></td><td " + jslink + ">" + (mail.hasAttachment() ? "<img src=\"/susimail/icons/attach.png\" alt=\"\" title=\"" + _t("Message has an attachment") + "\">" : " ") + "</td><td " + jslink + ">" + link + mail.shortSubject + "</a></td><td " + jslink + ">" + (mail.isSpam() ? "<img src=\"/susimail/icons/flag_red.png\" alt=\"\" title=\"" + _t("Message is spam") + "\">" : " ") + "</td><td " + jslink + ">" + // don't let date get split across lines mail.localFormattedDate.replace(" ", " ") + "</td><td " + jslink + "> </td><td align=\"right\" " + jslink + ">" + ((mail.getSize() > 0) ? (DataHelper.formatSize2(mail.getSize()) + 'B') : "???") + "</td></tr>" ); bg = 1 - bg; i++; } if (i == 0) out.println("<tr><td colspan=\"9\" align=\"center\"><i>" + _t("No messages") + "</i></td></tr>"); out.println( "<tr class=\"bottombuttons\"><td colspan=\"9\"><hr></td></tr>"); if (sessionObject.folder.getPages() > 1 && i > 30) { // show the buttons again if page is big out.println("<tr class=\"bottombuttons\"><td colspan=\"9\" align=\"center\">"); showPageButtons(out, sessionObject.folder); out.println("</td></tr>"); } out.println("<tr class=\"bottombuttons\"><td colspan=\"5\" align=\"left\">"); if (i > 0) { if( sessionObject.reallyDelete ) { // TODO ngettext out.println("<p class=\"error\">" + _t("Really delete the marked messages?") + "</p>" + button( REALLYDELETE, _t("Yes, really delete them!") ) + "<br>" + button( CLEAR, _t("Cancel"))); } else { out.println(button( DELETE, _t("Delete Selected") ) + "<br>"); out.print( button( MARKALL, _t("Mark All") ) + " " + button( CLEAR, _t("Clear All") )); //"<br>" + //button( INVERT, _t("Invert Selection") ) + //"<br>"); } } out.print("</td>\n<td colspan=\"4\" align=\"right\">"); // moved to config page //out.print( // _t("Page Size") + ": <input type=\"text\" style=\"text-align: right;\" name=\"" + PAGESIZE + "\" size=\"4\" value=\"" + sessionObject.folder.getPageSize() + "\">" + // " " + // button( SETPAGESIZE, _t("Set") ) ); out.print("<br>"); out.print(button(CONFIGURE, _t("Settings"))); out.println("</td></tr>"); out.println( "</table>"); } /** * first prev next last */ private static void showPageButtons(PrintWriter out, Folder<?> folder) { out.println( "<br>" + ( folder.isFirstPage() ? button2( FIRSTPAGE, _t("First") ) + " " + button2( PREVPAGE, _t("Previous") ) : button( FIRSTPAGE, _t("First") ) + " " + button( PREVPAGE, _t("Previous") ) ) + "      " + _t("Page {0} of {1}", folder.getCurrentPage(), folder.getPages()) + "      " + ( folder.isLastPage() ? button2( NEXTPAGE, _t("Next") ) + " " + button2( LASTPAGE, _t("Last") ) : button( NEXTPAGE, _t("Next") ) + " " + button( LASTPAGE, _t("Last") ) ) ); } /** * * @param out * @param sessionObject */ private static void showMessage( PrintWriter out, SessionObject sessionObject ) { if( sessionObject.reallyDelete ) { out.println( "<p class=\"error\">" + _t("Really delete this message?") + " " + button( REALLYDELETE, _t("Yes, really delete it!") ) + "</p>" ); } Mail mail = sessionObject.mailCache.getMail( sessionObject.showUIDL, MailCache.FetchMode.ALL ); if(!RELEASE && mail != null && mail.hasBody()) { out.println( "<!--" ); out.println( "Debug: Mail header and body follow"); // FIXME encoding, escaping --, etc... but disabled. ReadBuffer body = mail.getBody(); out.println( quoteHTML( new String(body.content, body.offset, body.length ) ) ); out.println( "-->" ); } out.println("<div class=\"topbuttons\">"); out.println( button( NEW, _t("New") ) + spacer + button( REPLY, _t("Reply") ) + button( REPLYALL, _t("Reply All") ) + button( FORWARD, _t("Forward") ) + spacer + button( SAVE_AS, _t("Save As") ) + spacer); if (sessionObject.reallyDelete) out.println(button2(DELETE, _t("Delete"))); else out.println(button(DELETE, _t("Delete"))); out.println(spacer + button(LOGOUT, _t("Logout") )); out.println("<br>" + ( sessionObject.folder.isFirstElement( sessionObject.showUIDL ) ? button2( PREV, _t("Previous") ) : button( PREV, _t("Previous") ) ) + spacer + button( LIST, _t("Back to Folder") ) + spacer + ( sessionObject.folder.isLastElement( sessionObject.showUIDL ) ? button2( NEXT, _t("Next") ) : button( NEXT, _t("Next") ) )); out.println("</div>"); //if (Config.hasConfigFile()) // out.println(button( RELOAD, _t("Reload Config") ) + spacer); if( mail != null ) { out.println( "<table cellspacing=\"0\" cellpadding=\"5\">\n" + "<tr><td colspan=\"2\" align=\"center\"><hr></td></tr>\n" + "<tr class=\"mailhead\"><td align=\"right\" valign=\"top\">" + _t("From") + ":</td><td align=\"left\">" + quoteHTML( mail.sender ) + "</td></tr>\n" + "<tr class=\"mailhead\"><td align=\"right\" valign=\"top\">" + _t("Subject") + ":</td><td align=\"left\">" + quoteHTML( mail.formattedSubject ) + "</td></tr>\n" + "<tr class=\"mailhead\"><td align=\"right\" valign=\"top\">" + _t("Date") + ":</td><td align=\"left\">" + mail.quotedDate + "</td></tr>\n" + "<tr><td colspan=\"2\" align=\"center\"><hr></td></tr>" ); if( mail.hasPart()) { mail.setNew(false); showPart( out, mail.getPart(), 0, SHOW_HTML ); } else { out.println( "<tr class=\"mailbody\"><td colspan=\"2\" align=\"center\"><p class=\"error\">" + _t("Could not fetch mail body.") + "</p></td></tr>" ); } } else { out.println( "<tr class=\"mailbody\"><td colspan=\"2\" align=\"center\"><p class=\"error\">" + _t("Could not fetch mail.") + "</p></td></tr>" ); } out.println( "<tr><td colspan=\"2\" align=\"center\"><hr></td></tr>\n</table>" ); } /** * Simple configure page * * @since 0.9.13 */ private static void showConfig(PrintWriter out, SessionObject sessionObject) { int sz; if (sessionObject.folder != null) sz = sessionObject.folder.getPageSize(); else sz = Config.getProperty(Folder.PAGESIZE, Folder.DEFAULT_PAGESIZE); out.println("<div class=\"topbuttons\">"); out.println( _t("Folder Page Size") + ": <input type=\"text\" style=\"text-align: right;\" name=\"" + PAGESIZE + "\" size=\"4\" value=\"" + sz + "\">" + " " + button( SETPAGESIZE, _t("Set") ) ); out.println("<p>"); out.println("</div>"); out.print(_t("Advanced Configuration")); Properties config = Config.getProperties(); out.print(":</p><textarea cols=\"80\" rows=\"" + Math.max(8, config.size() + 2) + "\" spellcheck=\"false\" name=\"" + CONFIG_TEXT + "\">"); for (Map.Entry<Object, Object> e : config.entrySet()) { out.print(quoteHTML(e.getKey().toString())); out.print('='); out.println(quoteHTML(e.getValue().toString())); } out.println("</textarea>"); out.println("<div id=\"bottombuttons\">"); out.println("<br>"); out.println(button(SAVE, _t("Save Configuration"))); out.println(button(CANCEL, _t("Cancel"))); if (sessionObject.folder != null) out.println(spacer + button(LOGOUT, _t("Logout") )); out.println("</div>"); } /** translate */ private static String _t(String s) { return Messages.getString(s); } /** translate */ private static String _t(String s, Object o) { return Messages.getString(s, o); } /** translate */ private static String _t(String s, Object o, Object o2) { return Messages.getString(s, o, o2); } /** translate */ private static String ngettext(String s, String p, int n) { return Messages.getString(n, s, p); } /** * Get all themes * @return String[] -- Array of all the themes found. */ private static String[] getThemes() { String[] themes = null; // "docs/themes/susimail/" File dir = new File(I2PAppContext.getGlobalContext().getBaseDir(), "docs/themes/susimail"); FileFilter fileFilter = new FileFilter() { public boolean accept(File file) { return file.isDirectory(); } }; // Walk the themes dir, collecting the theme names, and append them to the map File[] dirnames = dir.listFiles(fileFilter); if (dirnames != null) { themes = new String[dirnames.length]; for(int i = 0; i < dirnames.length; i++) { themes[i] = dirnames[i].getName(); } } // return the map. return themes; } }