Changeset 844977c


Ignore:
Timestamp:
Apr 14, 2018 3:50:07 PM (2 years ago)
Author:
zzz <zzz@…>
Branches:
master
Children:
8c0e82d
Parents:
ffad52e
Message:

SusiMail?: Add folders, drafts, background sending (ticket #2087)
Use with caution; cleanups and CSS to follow

Files:
1 added
12 edited

Legend:

Unmodified
Added
Removed
  • apps/susimail/src/src/i2p/susi/util/MemoryBuffer.java

    rffad52e r844977c  
    8787        @Override
    8888        public String toString() {
    89                 return "SB " + (content == null ? "empty" : content.length + " bytes");
     89                return "MB " + (content == null ? "empty" : content.length + " bytes");
    9090        }
    9191}
  • apps/susimail/src/src/i2p/susi/webmail/Attachment.java

    rffad52e r844977c  
    7474
    7575        /**
     76         * @return absolute path to the data file
     77         * @since 0.9.35
     78         */
     79        public String getPath() {
     80                return data.getAbsolutePath();
     81        }
     82
     83        /**
    7684         * The unencoded size
    7785         * @since 0.9.33
  • apps/susimail/src/src/i2p/susi/webmail/Mail.java

    rffad52e r844977c  
    301301                if( text != null && text.length() > 0 ) {                       
    302302                        String[] ccs = DataHelper.split(text, ",");
     303                        ok = getRecipientsFromList(recipients, ccs, ok);
     304                }
     305                return ok;
     306        }
     307
     308        /**
     309         * A little misnamed. Adds all addresses from the elements
     310         * in text to the recipients list.
     311         *
     312         * @param recipients out param
     313         * @param ok will be returned
     314         * @return true if ALL e-mail addresses are valid AND the in parameter was true
     315         * @since 0.9.35
     316         */
     317        public static boolean getRecipientsFromList( ArrayList<String> recipients, String[] ccs, boolean ok )
     318        {
     319                if (ccs != null && ccs.length > 0 ) {                   
    303320                        for( int i = 0; i < ccs.length; i++ ) {
    304321                                String recipient = ccs[i].trim();
  • apps/susimail/src/src/i2p/susi/webmail/MailCache.java

    rffad52e r844977c  
    2828import i2p.susi.util.Buffer;
    2929import i2p.susi.util.FileBuffer;
     30import i2p.susi.util.Folder;
     31import i2p.susi.util.Folder.SortOrder;
    3032import i2p.susi.util.ReadBuffer;
    3133import i2p.susi.util.MemoryBuffer;
     34import static i2p.susi.webmail.Sorters.*;
    3235import i2p.susi.webmail.pop3.POP3MailBox;
    3336import i2p.susi.webmail.pop3.POP3MailBox.FetchRequest;
     
    4447
    4548import net.i2p.I2PAppContext;
     49import net.i2p.util.FileUtil;
    4650import net.i2p.util.I2PAppThread;
    4751
    4852/**
     53 * There's one of these for each Folder.
     54 * However, only DIR_FOLDER has a non-null POP3MailBox.
     55 *
    4956 * @author user
    5057 */
     
    5966        private final PersistentMailCache disk;
    6067        private final I2PAppContext _context;
     68        private final Folder<String> folder;
     69        private final String folderName;
    6170        private NewMailListener _loadInProgress;
     71        private boolean _isLoaded;
     72        private final boolean _isDrafts;
    6273       
    6374        /** Includes header, headers are generally 1KB to 1.5 KB,
     
    7081         * Does NOT load the mails in. Caller MUST call loadFromDisk().
    7182         *
    72          * @param mailbox non-null
    73          */
    74         MailCache(I2PAppContext ctx, POP3MailBox mailbox,
     83         * @param mailbox non-null for DIR_FOLDER; null otherwise
     84         */
     85        MailCache(I2PAppContext ctx, POP3MailBox mailbox, String folderName,
    7586                  String host, int port, String user, String pass) throws IOException {
    7687                this.mailbox = mailbox;
    7788                mails = new Hashtable<String, Mail>();
    78                 disk = new PersistentMailCache(ctx, host, port, user, pass, PersistentMailCache.DIR_FOLDER);
     89                disk = new PersistentMailCache(ctx, host, port, user, pass, folderName);
    7990                // TODO Drafts, Sent, Trash
    8091                _context = ctx;
     92                Folder<String> folder = new Folder<String>();   
     93                // setElements() sorts, so configure the sorting first
     94                //sessionObject.folder.addSorter( SORT_ID, new IDSorter( sessionObject.mailCache ) );
     95                folder.addSorter(WebMail.SORT_SENDER, new SenderSorter(this));
     96                folder.addSorter(WebMail.SORT_SUBJECT, new SubjectSorter(this));
     97                folder.addSorter(WebMail.SORT_DATE, new DateSorter(this));
     98                folder.addSorter(WebMail.SORT_SIZE, new SizeSorter(this));
     99                // reverse sort, latest mail first
     100                // TODO get user defaults from config
     101                folder.setSortBy(WebMail.SORT_DEFAULT, WebMail.SORT_ORDER_DEFAULT);
     102                this.folder = folder;
     103                this.folderName = folderName;
     104                _isDrafts = folderName.equals(WebMail.DIR_DRAFTS);
     105        }
     106
     107        /**
     108         * @since 0.9.35
     109         * @return as passed in
     110         */
     111        public String getFolderName() {
     112                return folderName;
     113        }
     114
     115        /**
     116         * @since 0.9.35
     117         * @return translation of name passed in
     118         */
     119        public String getTranslatedName() {
     120                String rv = folderName.equals(WebMail.DIR_FOLDER) ? "Inbox" : folderName;
     121                return Messages.getString(rv);
     122        }
     123
     124        /**
     125         * @since 0.9.35
     126         * @return non-null
     127         */
     128        public Folder<String> getFolder() {
     129                return folder;
     130        }
     131
     132        /**
     133         * For writing a new full mail (NOT headers only)
     134         * Caller must close.
     135         * @since 0.9.35
     136         */
     137        public Buffer getFullWriteBuffer(String uidl) {
     138                // no locking this way
     139                return disk.getFullBuffer(uidl);
     140        }
     141
     142        /**
     143         * For writing a new full mail
     144         * @param buffer as received from getFullBuffer
     145         * @since 0.9.35
     146         */
     147        public void writeComplete(String uidl, Buffer buffer, boolean success) {
     148                buffer.writeComplete(success);
     149                if (success) {
     150                        Mail mail;
     151                        if (_isDrafts)
     152                                mail = new Draft(uidl);
     153                        else
     154                                mail = new Mail(uidl);
     155                        mail.setBody(buffer);
     156                        synchronized(mails) {
     157                                mails.put(uidl, mail);
     158                        }
     159                        folder.addElement(uidl);
     160                }
     161        }
     162
     163        /**
     164         * @return non-null only for Drafts
     165         * @since 0.9.35
     166         */
     167        public File getAttachmentDir() {
     168                return disk.getAttachmentDir();
     169        }
     170
     171        /**
     172         * Move a mail to another MailCache, neither may be DIR_DRAFTS
     173         * @return success
     174         * @since 0.9.35
     175         */
     176        public boolean moveTo(String uidl, MailCache toMC) {
     177                if (folderName.equals(WebMail.DIR_DRAFTS) ||
     178                    toMC.getFolderName().equals(WebMail.DIR_DRAFTS))
     179                        return false;
     180                Mail mail;
     181                synchronized(mails) {
     182                        mail = mails.get(uidl);
     183                        if (mail == null)
     184                                return false;
     185                        if (!mail.hasBody())
     186                                return false;
     187                        File from = disk.getFullFile(uidl);
     188                        if (!from.exists())
     189                                return false;
     190                        PersistentMailCache toPMC = toMC.disk;
     191                        File to = toPMC.getFullFile(uidl);
     192                        if (to.exists())
     193                                return false;
     194                        if (!FileUtil.rename(from, to))
     195                                return false;
     196                        mails.remove(uidl);
     197                        folder.removeElement(uidl);
     198                }
     199                toMC.movedTo(mail);
     200                if (mailbox != null)
     201                        mailbox.queueForDeletion(mail.uidl);
     202                return true;
     203        }
     204
     205        /**
     206         * Moved a mail from another MailCache
     207         * @since 0.9.35
     208         */
     209        private void movedTo(Mail mail) {
     210                synchronized(mails) {
     211                        // we must reset the body of the mail to the new FileBuffer
     212                        Buffer body = disk.getFullBuffer(mail.uidl);
     213                        mail.setBody(body);
     214                        mails.put(mail.uidl, mail);
     215                }
     216                folder.addElement(mail.uidl);
     217        }
     218
     219        /**
     220         * Is loadFromDisk in progress?
     221         * @since 0.9.35
     222         */
     223        public synchronized boolean isLoading() {
     224                return _loadInProgress != null;
     225        }
     226
     227        /**
     228         * Has loadFromDisk completed?
     229         * @since 0.9.35
     230         */
     231        public synchronized boolean isLoaded() {
     232                return _isLoaded;
    81233        }
    82234
     
    89241         */
    90242        public synchronized boolean loadFromDisk(NewMailListener nml) {
    91                 if (_loadInProgress != null)
     243                if (_isLoaded || _loadInProgress != null)
    92244                        return false;
     245                Debug.debug(Debug.DEBUG, "Loading folder " + folderName);
    93246                Thread t = new I2PAppThread(new LoadMailRunner(nml), "Email loader");
    94247                _loadInProgress = nml;
     
    116269                                if(!mails.isEmpty())
    117270                                        result = true;
     271                                Debug.debug(Debug.DEBUG, "Folder loaded: " + folderName);
    118272                        } finally {
    119273                                synchronized(MailCache.this) {
    120274                                        if (_loadInProgress == _nml)
    121275                                                _loadInProgress = null;
     276                                        _isLoaded = true;
    122277                                }
    123278                                _nml.foundNewMail(result);
     
    134289        private void blockingLoadFromDisk() {
    135290                synchronized(mails) {
    136                         if (!mails.isEmpty())
     291                        if (_isLoaded)
    137292                                throw new IllegalStateException();
    138293                        Collection<Mail> dmails = disk.getMails();
     
    165320
    166321        /**
    167          * Fetch any needed data from pop3 server, unless mode is CACHE_ONLY.
     322         * Fetch any needed data from pop3 server, unless mode is CACHE_ONLY,
     323         * or this isn't the Inbox.
    168324         * Blocking unless mode is CACHE_ONLY.
    169325         *
     
    174330        @SuppressWarnings({"unchecked", "rawtypes"})
    175331        public Mail getMail(String uidl, FetchMode mode) {
    176                
    177332                Mail mail = null, newMail = null;
    178333
     
    183338                        mail = mails.get( uidl );
    184339                        if( mail == null ) {
     340                                // if not in inbox, we can't fetch, this is what we have
     341                                if (mailbox == null)
     342                                        return null;
    185343                                newMail = new Mail(uidl);
    186344                                // TODO really?
     
    194352                if (mail.markForDeletion)
    195353                        return null;
     354                // if not in inbox, we can't fetch, this is what we have
     355                if (mailbox == null)
     356                        return mail;
     357
    196358                long sz = mail.getSize();
    197359                if (mode == FetchMode.HEADER && sz > 0 && sz <= FETCH_ALL_SIZE)
     
    199361                       
    200362                if (mode == FetchMode.HEADER) {
    201                         if(!mail.hasHeader())
    202                                 mail.setHeader(mailbox.getHeader(uidl));
     363                        if (!mail.hasHeader()) {
     364                                Buffer buf = mailbox.getHeader(uidl);
     365                                if (buf != null)
     366                                        mail.setHeader(buf);
     367                        }
    203368                } else if (mode == FetchMode.ALL) {
    204369                        if(!mail.hasBody()) {
     
    224389         * After this, call getUIDLs() to get all known mail UIDLs.
    225390         * MUST already be connected, otherwise returns false.
     391         * Call only on inbox!
    226392         *
    227393         * Blocking.
     
    235401                if (mode == FetchMode.CACHE_ONLY)
    236402                        throw new IllegalArgumentException();
     403                if (mailbox == null) {
     404                        Debug.debug(Debug.DEBUG, "getMail() mode " + mode + " called on wrong folder " + getFolderName(), new Exception());
     405                        return false;
     406                }
    237407                boolean hOnly = mode == FetchMode.HEADER;
    238408               
     
    349519         * Send delete requests to POP3 then quit and reconnect.
    350520         * No success/failure indication is returned.
     521         * Does not delete from folder.
    351522         *
    352523         * @since 0.9.13
     
    360531         * Send delete requests to POP3 then quit and reconnect.
    361532         * No success/failure indication is returned.
     533         * Does not delete from folder.
    362534         *
    363535         * @since 0.9.13
     
    382554                if (toDelete.isEmpty())
    383555                        return;
    384                 mailbox.queueForDeletion(toDelete);
     556                if (mailbox != null)
     557                        mailbox.queueForDeletion(toDelete);
    385558        }
    386559
  • apps/susimail/src/src/i2p/susi/webmail/MailPart.java

    rffad52e r844977c  
    302302
    303303        /**
     304         *  Synched because FileBuffer keeps stream open
     305         *
    304306         *  @param offset 2 for sendAttachment, 0 otherwise, probably for \r\n
    305307         *  @since 0.9.13
    306308         */
    307         public void decode(int offset, Buffer out) throws IOException {
     309        public synchronized void decode(int offset, Buffer out) throws IOException {
    308310                String encg = encoding;
    309311                if (encg == null) {
     
    320322                Buffer dout = null;
    321323                try {
    322                         in = buffer.getInputStream();
    323                         DataHelper.skip(in, buffer.getOffset() + beginBody + offset);
    324                         lin = new LimitInputStream(in, end - beginBody - offset);
     324                        lin = getRawInputStream(offset);
    325325                        if (decodedLength < 0) {
    326326                                cos = new CountingOutputStream(out.getOutputStream());
     
    342342                        throw ioe;
    343343                } finally {
    344                         if (in != null) try { in.close(); } catch (IOException ioe) {};
    345344                        if (lin != null) try { lin.close(); } catch (IOException ioe) {};
    346345                        buffer.readComplete(true);
     
    353352                if (cos != null)
    354353                        decodedLength = (int) cos.getWritten();
     354        }
     355
     356        /**
     357         *  Synched because FileBuffer keeps stream open
     358         *  Caller must close out
     359         *
     360         *  @since 0.9.35
     361         */
     362        public synchronized void outputRaw(OutputStream out) throws IOException {
     363                LimitInputStream lin = null;
     364                try {
     365                        lin = getRawInputStream(0);
     366                        DataHelper.copy(lin, out);
     367                } catch (IOException ioe) {
     368                        Debug.debug(Debug.DEBUG, "Decode IOE", ioe);
     369                        throw ioe;
     370                } finally {
     371                        if (lin != null) try { lin.close(); } catch (IOException ioe) {};
     372                        buffer.readComplete(true);
     373                }
     374        }
     375
     376        /**
     377         *  Synched because FileBuffer keeps stream open
     378         *  Caller must call readComplete() on buffer
     379         *
     380         *  @param offset 2 for sendAttachment, 0 otherwise, probably for \r\n
     381         *  @since 0.9.35
     382         */
     383        private synchronized LimitInputStream getRawInputStream(int offset) throws IOException {
     384                InputStream in = buffer.getInputStream();
     385                DataHelper.skip(in, buffer.getOffset() + beginBody + offset);
     386                return new LimitInputStream(in, end - beginBody - offset);
    355387        }
    356388
  • apps/susimail/src/src/i2p/susi/webmail/PersistentMailCache.java

    rffad52e r844977c  
    5353 * each file to a flat directory.
    5454 *
    55  * TODO draft and sent folders, cached server caps and config.
     55 * This class should only be accessed from MailCache.
     56 *
     57 * TODO cached server caps and config.
    5658 *
    5759 * @since 0.9.14
     
    6971        private final Object _lock;
    7072        private final File _cacheDir;
     73        // non-null only for Drafts
     74        private final File _attachmentDir;
    7175        private final I2PAppContext _context;
     76        private final boolean _isDrafts;
    7277
    7378        private static final String DIR_SUSI = "susimail";
    7479        private static final String DIR_CACHE = "cache";
    7580        private static final String CACHE_PREFIX = "cache-";
    76         public static final String DIR_FOLDER = "cur"; // MailDir-like
    77         public static final String DIR_DRAFTS = "Drafts"; // MailDir-like
    78         public static final String DIR_SENT = "Sent"; // MailDir-like
    79         public static final String DIR_TRASH = "Trash"; // MailDir-like
    80         public static final String DIR_SPAM = "Bulk Mail"; // MailDir-like
    8181        public static final String DIR_IMPORT = "import"; // Flat with .eml files, debug only for now
     82        public static final String DIR_ATTACHMENTS = "attachments"; // Flat with draft attachment files
    8283        private static final String DIR_PREFIX = "s";
    8384        private static final String FILE_PREFIX = "mail-";
     
    9293         *
    9394         *  @param pass ignored
    94          *  @param folder use DIR_FOLDER
     95         *  @param folder e.g. DIR_FOLDER
    9596         */
    9697        public PersistentMailCache(I2PAppContext ctx, String host, int port, String user, String pass, String folder) throws IOException {
    9798                _context = ctx;
     99                _isDrafts = folder.equals(WebMail.DIR_DRAFTS);
    98100                _lock = getLock(host, port, user, pass);
    99101                synchronized(_lock) {
    100102                        _cacheDir = makeCacheDirs(host, port, user, pass, folder);
    101103                        // Debugging only for now.
    102                         if (folder.equals(DIR_FOLDER))
     104                        File attach = null;
     105                        if (folder.equals(WebMail.DIR_FOLDER)) {
    103106                                importMail();
     107                        } else if (folder.equals(WebMail.DIR_DRAFTS)) {
     108                                attach = new SecureDirectory(_cacheDir, DIR_ATTACHMENTS);
     109                                attach.mkdirs();
     110                        }
     111                        _attachmentDir = attach;
    104112                }
    105113        }
     
    144152                List<Thread> threads = new ArrayList<Thread>(tcnt);
    145153                for (int i = 0; i < tcnt; i++) {
    146                         Thread t = new I2PAppThread(new Loader(fq, rv), "Email loader " + i);
     154                        Thread t = new I2PAppThread(new Loader(fq, rv, _isDrafts), "Email loader " + i);
    147155                        t.start();
    148156                        threads.add(t);
     
    167175                private final Queue<File> _in;
    168176                private final Queue<Mail> _out;
    169 
    170                 public Loader(Queue<File> in, Queue<Mail> out) {
     177                private final boolean _isD;
     178
     179                public Loader(Queue<File> in, Queue<Mail> out, boolean isDrafts) {
    171180                        _in = in; _out = out;
     181                        _isD = isDrafts;
    172182                }
    173183
     
    175185                        File f;
    176186                        while ((f = _in.poll()) != null) {
    177                                 Mail mail = load(f);
     187                                Mail mail = load(f, _isD);
    178188                                if (mail != null)
    179189                                        _out.offer(mail);
     
    297307        }
    298308
    299         private File getHeaderFile(String uidl) {
     309        public File getHeaderFile(String uidl) {
    300310                return getFile(uidl, HDR_SUFFIX);
    301311        }
    302312
    303         private File getFullFile(String uidl) {
     313        public File getFullFile(String uidl) {
    304314                return getFile(uidl, FULL_SUFFIX);
     315        }
     316
     317        /**
     318         * For reading or writing a new full mail (NOT headers only).
     319         * For writing, caller MUST call writeComplete() on rv.
     320         * Does not necessarily exist.
     321         *
     322         * @since 0.9.35
     323         */
     324        public GzipFileBuffer getFullBuffer(String uidl) {
     325                return new GzipFileBuffer(getFile(uidl, FULL_SUFFIX));
     326        }
     327
     328        /**
     329         * @return non-null only for Drafts
     330         * @since 0.9.35
     331         */
     332        public File getAttachmentDir() {
     333                return _attachmentDir;
    305334        }
    306335
     
    352381         *  @return null on failure
    353382         */
    354         private static Mail load(File f) {
     383        private static Mail load(File f, boolean isDrafts) {
    355384                String name = f.getName();
    356385                String uidl;
     
    370399                if (rb == null)
    371400                        return null;
    372                 Mail mail = new Mail(uidl);
     401                Mail mail;
     402                if (isDrafts)
     403                        mail = new Draft(uidl);
     404                else
     405                        mail = new Mail(uidl);
    373406                if (headerOnly)
    374407                        mail.setHeader(rb);
  • apps/susimail/src/src/i2p/susi/webmail/WebMail.java

    rffad52e r844977c  
    4444
    4545import java.io.BufferedReader;
     46import java.io.BufferedWriter;
    4647import java.io.ByteArrayInputStream;
     48import java.io.ByteArrayOutputStream;
    4749import java.io.File;
    4850import java.io.FileFilter;
     
    5153import java.io.InputStreamReader;
    5254import java.io.OutputStream;
     55import java.io.OutputStreamWriter;
    5356import java.io.PrintWriter;
    5457import java.io.Serializable;
     
    6265import java.util.Date;
    6366import java.util.Enumeration;
     67import java.util.HashMap;
    6468import java.util.Iterator;
    6569import java.util.List;
     
    137141        private static final String SUSI_NONCE = "susiNonce";
    138142        private static final String B64UIDL = "msg";
     143        private static final String NEW_UIDL = "newmsg";
    139144        private static final String PREV_B64UIDL = "prevmsg";
    140145        private static final String NEXT_B64UIDL = "nextmsg";
     
    142147        private static final String NEXT_PAGE_NUM = "nextpagenum";
    143148        private static final String CURRENT_SORT = "currentsort";
    144         private static final String CURRENT_FOLDER = "currentfolder";
     149        private static final String CURRENT_FOLDER = "folder";
     150        private static final String NEW_FOLDER  = "newfolder";
     151        private static final String DRAFT_EXISTS = "draftexists";
    145152        private static final String DEBUG_STATE = "currentstate";
    146153
     
    161168        private static final String DELETE = "delete";
    162169        private static final String REALLYDELETE = "really_delete";
     170        private static final String MOVE_TO = "moveto";
     171        private static final String SWITCH_TO = "switchto";
    163172        // also a GET param
    164173        private static final String SHOW = "show";
     
    178187       
    179188        private static final String SEND = "send";
     189        private static final String SAVE_AS_DRAFT = "saveasdraft";
    180190        private static final String CANCEL = "cancel";
    181191        private static final String DELETE_ATTACHMENT = "delete_attachment";
     
    197207        // SORT is a GET or POST param, SORT_XX are the values, possibly prefixed by '-'
    198208        private static final String SORT = "sort";
    199         private static final String SORT_ID = "id";
    200         private static final String SORT_SENDER = "sender";
    201         private static final String SORT_SUBJECT = "subject";
    202         private static final String SORT_DATE = "date";
    203         private static final String SORT_SIZE = "size";
    204         private static final String SORT_DEFAULT = SORT_DATE;
    205         private static final SortOrder SORT_ORDER_DEFAULT = SortOrder.UP;
     209        static final String SORT_ID = "id";
     210        static final String SORT_SENDER = "sender";
     211        static final String SORT_SUBJECT = "subject";
     212        static final String SORT_DATE = "date";
     213        static final String SORT_SIZE = "size";
     214        static final String SORT_DEFAULT = SORT_DATE;
     215        static final SortOrder SORT_ORDER_DEFAULT = SortOrder.UP;
    206216        // for XSS
    207217        private static final List<String> VALID_SORTS = Arrays.asList(new String[] {
    208218                                 SORT_ID, SORT_SENDER, SORT_SUBJECT, SORT_DATE, SORT_SIZE,
    209219                                 '-' + SORT_ID, '-' + SORT_SENDER, '-' + SORT_SUBJECT, '-' + SORT_DATE, '-' + SORT_SIZE });
     220
     221        static final String DIR_FOLDER = "cur"; // MailDir-like
     222        public static final String DIR_DRAFTS = _x("Drafts"); // MailDir-like
     223        private static final String DIR_SENT = _x("Sent"); // MailDir-like
     224        private static final String DIR_TRASH = _x("Trash"); // MailDir-like
     225        private static final String DIR_SPAM = _x("Bulk Mail"); // MailDir-like
     226        // internal/on-disk names
     227        private static final String[] DIRS = { DIR_FOLDER, DIR_DRAFTS, DIR_SENT, DIR_TRASH, DIR_SPAM };
     228        // untranslated, translate on use
     229        private static final String[] DISPLAY_DIRS = { _x("Inbox"), DIR_DRAFTS, DIR_SENT, DIR_TRASH, DIR_SPAM };
    210230
    211231        private static final String CONFIG_TEXT = "config_text";
     
    262282                int smtpPort;
    263283                POP3MailBox mailbox;
    264                 MailCache mailCache;
    265                 Folder<String> folder;
    266                 boolean isLoading, isFetching;
     284                final Map<String, MailCache> caches;
     285                boolean isFetching;
    267286                /** Set by threaded connector. Error or null */
    268287                String connectError;
    269288                /** Set by threaded connector. -1 if nothing to report, 0 or more after fetch complete */
    270289                int newMails = -1;
    271                 String user, pass, host, error, info;
     290                String user, pass, host, error = "", info = "";
    272291                String replyTo, replyCC;
    273292                String subject, body;
    274                 public StringBuilder sentMail;
     293                // TODO Map of UIDL to List
    275294                public ArrayList<Attachment> attachments;
    276295                // This is only for multi-delete. Single-message delete is handled with P-R-G
     
    286305                        bccToSelf = Boolean.parseBoolean(Config.getProperty( CONFIG_BCC_TO_SELF, "true" ));
    287306                        nonces = new ArrayList<String>(MAX_NONCES + 1);
     307                        caches = new HashMap<String, MailCache>(8);
    288308                }
    289309
     
    314334                        if (!yes)
    315335                                return;
    316                         MailCache mc = mailCache;
    317                         Folder<String> f = folder;
    318                         if (mc != null && f != null) {
     336                        MailCache mc = caches.get(DIR_FOLDER);
     337                        if (mc != null) {
    319338                                String[] uidls = mc.getUIDLs();
    320                                 f.addElements(Arrays.asList(uidls));
     339                                mc.getFolder().addElements(Arrays.asList(uidls));
    321340                        }
    322341                }
     
    341360                }
    342361
    343                 /** @since 0.9.33 */
     362                /**
     363                 * Remove references but does not delete files
     364                 * @since 0.9.33
     365                 */
    344366                public void clearAttachments() {
     367                        if (attachments != null) {
     368                                attachments.clear();
     369                        }
     370                }
     371
     372                /**
     373                 * Remove references AND delete files
     374                 * @since 0.9.35
     375                 */
     376                public void deleteAttachments() {
    345377                        if (attachments != null) {
    346378                                for (Attachment a : attachments) {
     
    364396                buf.append("<input type=\"submit\" class=\"").append(name).append("\" name=\"")
    365397                   .append(name).append("\" value=\"").append(label).append('"');
    366                 if (name.equals(SEND) || name.equals(CANCEL) || name.equals(DELETE_ATTACHMENT) || name.equals(NEW_UPLOAD) ||
     398                if (name.equals(SEND) || name.equals(CANCEL) || name.equals(DELETE_ATTACHMENT) ||
     399                    name.equals(NEW_UPLOAD) || name.equals(SAVE_AS_DRAFT) ||  // compose page
    367400                    name.equals(SETPAGESIZE) || name.equals(SAVE))  // config page
    368401                        buf.append(" onclick=\"beforePopup=false;\"");
     
    428461        {
    429462                String value = request.getParameter( key );
    430                 return value != null && (value.length() > 0 || key.equals(CONFIGURE));
     463                return value != null && (value.length() > 0 || key.equals(CONFIGURE) || key.equals(NEW_UIDL));
    431464        }
    432465        /**
     
    750783                MailCache mc;
    751784                try {
    752                         mc = new MailCache(ctx, mailbox, host, pop3PortNo, user, pass);
     785                        mc = new MailCache(ctx, mailbox, DIR_FOLDER,
     786                                           host, pop3PortNo, user, pass);
     787                        sessionObject.caches.put(DIR_FOLDER, mc);
     788                        MailCache mc2 = new MailCache(ctx, null, DIR_DRAFTS,
     789                                                      host, pop3PortNo, user, pass);
     790                        sessionObject.caches.put(DIR_DRAFTS, mc2);
     791                        mc2 = new MailCache(ctx, null, DIR_SENT,
     792                                            host, pop3PortNo, user, pass);
     793                        sessionObject.caches.put(DIR_SENT, mc2);
     794                        mc2 = new MailCache(ctx, null, DIR_TRASH,
     795                                            host, pop3PortNo, user, pass);
     796                        sessionObject.caches.put(DIR_TRASH, mc2);
     797                        mc2 = new MailCache(ctx, null, DIR_SPAM,
     798                                            host, pop3PortNo, user, pass);
     799                        sessionObject.caches.put(DIR_SPAM, mc2);
    753800                } catch (IOException ioe) {
    754801                        Debug.debug(Debug.ERROR, "Error creating disk cache", ioe);
     
    756803                        return State.AUTH;
    757804                }
    758                 Folder<String> folder = new Folder<String>();   
    759                 // setElements() sorts, so configure the sorting first
    760                 //sessionObject.folder.addSorter( SORT_ID, new IDSorter( sessionObject.mailCache ) );
    761                 folder.addSorter( SORT_SENDER, new SenderSorter(mc));
    762                 folder.addSorter( SORT_SUBJECT, new SubjectSorter(mc));
    763                 folder.addSorter( SORT_DATE, new DateSorter(mc));
    764                 folder.addSorter( SORT_SIZE, new SizeSorter(mc));
    765                 // reverse sort, latest mail first
    766                 // TODO get user defaults from config
    767                 folder.setSortBy(SORT_DEFAULT, SORT_ORDER_DEFAULT);
    768                 sessionObject.folder = folder;
    769805
    770806                sessionObject.mailbox = mailbox;
     
    776812                // Thread the loading and the server connection.
    777813                // Either could finish first.
     814                // We only load the inbox here. Others are loaded on-demand in processRequest()
    778815
    779816                // With a mix of email (10KB median, 100KB average size),
    780817                // about 20 emails per second per thread loaded.
    781818                // thread 1: mc.loadFromDisk()
    782                 sessionObject.mailCache = mc;
    783                 sessionObject.isLoading = true;
    784                 boolean ok = mc.loadFromDisk(new LoadWaiter(sessionObject));
    785                 if (!ok)
    786                         sessionObject.isLoading = false;
     819                boolean ok = mc.loadFromDisk(new LoadWaiter(sessionObject, mc));
    787820
    788821                // thread 2: mailbox.connectToServer()
     
    798831
    799832                // wait a little while so we avoid the loading page if we can
    800                 if (sessionObject.isLoading) {
     833                if (ok && mc.isLoading()) {
    801834                        try {
    802835                                sessionObject.wait(5000);
     
    805838                        }
    806839                }
    807                 state = sessionObject.isLoading ? State.LOADING : State.LIST;
     840                state = mc.isLoading() ? State.LOADING : State.LIST;
    808841                return state;
    809842        }
     
    815848        private static class LoadWaiter implements NewMailListener {
    816849                private final SessionObject _so;
    817 
    818                 public LoadWaiter(SessionObject so) {
     850                private final MailCache _mc;
     851
     852                public LoadWaiter(SessionObject so, MailCache mc) {
    819853                        _so = so;
     854                        _mc = mc;
    820855                }
    821856
     
    823858                        synchronized(_so) {
    824859                                // get through cache so we have the disk-only ones too
    825                                 MailCache mc = _so.mailCache;
    826                                 Folder<String> f = _so.folder;
    827                                 if (mc != null && f != null) {
    828                                         String[] uidls = mc.getUIDLs();
    829                                         int added = f.addElements(Arrays.asList(uidls));
    830                                         if (added > 0)
    831                                                 _so.pageChanged = true;
    832                                         Debug.debug(Debug.DEBUG, "Folder loaded");
    833                                 } else {
    834                                         Debug.debug(Debug.DEBUG, "MailCache/folder vanished?");
    835                                 }
    836                                 _so.isLoading = false;
     860                                Folder<String> f = _mc.getFolder();
     861                                String[] uidls = _mc.getUIDLs();
     862                                int added = f.addElements(Arrays.asList(uidls));
     863                                if (added > 0)
     864                                        _so.pageChanged = true;
    837865                                _so.notifyAll();
    838866                        }
     
    868896                                // because we may already have UIDLs in the MailCache to fetch
    869897                                synchronized(_so) {
    870                                         while (_so.isLoading) {
     898                                        mc = _so.caches.get(DIR_FOLDER);
     899                                        while (mc.isLoading()) {
    871900                                                try {
    872901                                                        _so.wait(5000);
     
    876905                                                }
    877906                                        }
    878                                         mc = _so.mailCache;
    879                                         f = _so.folder;
    880907                                }
    881908                                Debug.debug(Debug.DEBUG, "Done waiting for folder load");
    882909                                // fetch the mail outside the lock
    883910                                // TODO, would be better to add each email as we get it
    884                                 if (mc != null && f != null) {
     911                                if (mc != null) {
    885912                                        found = mc.getMail(MailCache.FetchMode.HEADER);
    886913                                }
     
    901928                                } else if (mc != null && f != null) {
    902929                                        String[] uidls = mc.getUIDLs();
    903                                         int added = f.addElements(Arrays.asList(uidls));
     930                                        int added = mc.getFolder().addElements(Arrays.asList(uidls));
    904931                                        if (added > 0)
    905932                                                _so.pageChanged = true;
     
    935962                                mailbox.destroy();
    936963                                sessionObject.mailbox = null;
    937                                 sessionObject.mailCache = null;
     964                                sessionObject.caches.clear();
    938965                        }
    939966                        sessionObject.info += _t("User logged out.") + '\n';
     
    972999                        return state;
    9731000                // if loading, we can't get to states LIST/SHOW or it will block
    974                 if (sessionObject.isLoading)
    975                         return State.LOADING;
     1001                for (MailCache mc : sessionObject.caches.values()) {
     1002                        if (mc.isLoading())
     1003                               return State.LOADING;
     1004                }
    9761005
    9771006                /*
     
    9821011                        // We have to make sure to get the state right even if
    9831012                        // the user hit the back button previously
    984                         if( buttonPressed( request, SEND ) ) {
    985                                 if (sendMail(sessionObject, request)) {
     1013                        if (buttonPressed(request, SEND) || buttonPressed(request, SAVE_AS_DRAFT)) {
     1014                                // always save as draft before sending
     1015                                String uidl = Base64.decodeToString(request.getParameter(NEW_UIDL));
     1016                                if (uidl == null)
     1017                                        uidl = I2PAppContext.getGlobalContext().random().nextLong() + "drft";
     1018                                Debug.debug(Debug.DEBUG, "Save as draft: " + uidl);
     1019                                MailCache toMC = sessionObject.caches.get(DIR_DRAFTS);
     1020                                Writer wout = null;
     1021                                boolean ok = false;
     1022                                Buffer buffer = null;
     1023                                try {
     1024                                        if (toMC == null)
     1025                                                throw new IOException("No Drafts folder?");
     1026                                        waitForLoad(sessionObject, toMC);
     1027                                        StringBuilder draft = composeDraft(sessionObject, request);
     1028                                        if (draft == null)
     1029                                                throw new IOException("Draft compose error");  // composeDraft added error messages
     1030                                        buffer = toMC.getFullWriteBuffer(uidl);
     1031                                        wout = new BufferedWriter(new OutputStreamWriter(buffer.getOutputStream(), "ISO-8859-1"));
     1032                                        SMTPClient.writeMail(wout, draft, null, null);
     1033                                        Debug.debug(Debug.DEBUG, "Saved as draft: " + uidl);
     1034                                        ok = true;
     1035                                } catch (IOException ioe) {
     1036                                        sessionObject.error += _t("Unable to save mail.") + ' ' + ioe.getMessage() + '\n';
     1037                                        Debug.debug(Debug.DEBUG, "Unable to save as draft: " + uidl, ioe);
     1038                                } finally {
     1039                                        if (wout != null) try { wout.close(); } catch (IOException ioe) {}
     1040                                        if (buffer != null)
     1041                                                toMC.writeComplete(uidl, buffer, ok);
     1042                                }
     1043                                if (ok) {
     1044                                        sessionObject.replyTo = null;
     1045                                        sessionObject.replyCC = null;
     1046                                        sessionObject.subject = null;
     1047                                        sessionObject.body = null;
     1048                                        sessionObject.clearAttachments();
     1049                                }
     1050                                if (ok && buttonPressed(request, SAVE_AS_DRAFT)) {
     1051                                        sessionObject.info += _t("Draft saved.") + '\n';
     1052                                } else if (ok && buttonPressed(request, SEND)) {
     1053                                        Draft mail = (Draft) toMC.getMail(uidl, MailCache.FetchMode.CACHE_ONLY);
     1054                                        if (mail != null) {
     1055                                                Debug.debug(Debug.DEBUG, "Send mail: " + uidl);
     1056                                                ok = sendMail(sessionObject, mail);
     1057                                        } else {
     1058                                                // couldn't read it back in?
     1059                                                ok = false;
     1060                                                sessionObject.error += _t("Unable to save mail.") + '\n';
     1061                                                Debug.debug(Debug.DEBUG, "Draft readback fail: " + uidl);
     1062                                        }
     1063                                }
     1064                                if (ok) {
    9861065                                        // If we have a reference UIDL, go back to that
    987                                         if (request.getParameter(B64UIDL) != null)
     1066                                        if (request.getParameter(B64UIDL) != null && buttonPressed(request, SEND))
    9881067                                                state = State.SHOW;
    9891068                                        else
    9901069                                                state = State.LIST;
    991                                 }
     1070                                } else {
     1071                                        state = State.NEW;
     1072                                }
     1073                                Debug.debug(Debug.DEBUG, "State after save as draft: " + state);
    9921074                        } else if (buttonPressed(request, CANCEL)) {
    9931075                                // If we have a reference UIDL, go back to that
     
    9961078                                else
    9971079                                        state = State.LIST;
    998                                 sessionObject.sentMail = null; 
    999                                 sessionObject.clearAttachments();
     1080                                sessionObject.replyTo = null;
     1081                                sessionObject.replyCC = null;
     1082                                sessionObject.subject = null;
     1083                                sessionObject.body = null;
     1084                                if (buttonPressed(request, DRAFT_EXISTS))
     1085                                        sessionObject.clearAttachments();
     1086                                else
     1087                                        sessionObject.deleteAttachments();
    10001088                        }
    10011089                }
     
    10221110                                state = State.SHOW;
    10231111                        } else if (buttonPressed(request, DELETE) ||
    1024                                    buttonPressed(request, REALLYDELETE)) {
     1112                                   buttonPressed(request, REALLYDELETE) ||
     1113                                   buttonPressed(request, MOVE_TO)) {
    10251114                                if (request.getParameter(B64UIDL) != null)
    10261115                                        state = State.SHOW;
     
    10301119                                if (request.getParameter(B64UIDL) != null)
    10311120                                        state = State.SHOW;
     1121                        } else if (buttonPressed(request, SWITCH_TO)) {
     1122                                        state = State.LIST;
    10321123                        }
    10331124                } else if (buttonPressed(request, DOWNLOAD) ||
     
    10541145                        if (buttonPressed(request, REPLY)) {
    10551146                                reply = true;
     1147                        } else if (buttonPressed(request, REPLYALL)) {
     1148                                replyAll = true;
     1149                        } else if(buttonPressed(request, FORWARD)) {
     1150                                forward = true;
     1151                        }
     1152                        if( reply || replyAll || forward ) {
    10561153                                state = State.NEW;
    1057                         }
    1058                         if( buttonPressed( request, REPLYALL ) ) {
    1059                                 replyAll = true;
    1060                                 state = State.NEW;
    1061                         }
    1062                         if( buttonPressed( request, FORWARD ) ) {
    1063                                 forward = true;
    1064                                 state = State.NEW;
    1065                         }
    1066                         if( reply || replyAll || forward ) {
    10671154                                /*
    10681155                                 * try to find message
     
    10831170                               
    10841171                                if( uidl != null ) {
    1085                                         Mail mail = sessionObject.mailCache.getMail( uidl, MailCache.FetchMode.ALL );
     1172                                        MailCache mc = getCurrentMailCache(sessionObject, request);
     1173                                        Mail mail = (mc != null) ? mc.getMail(uidl, MailCache.FetchMode.ALL) : null;
    10861174                                        /*
    10871175                                         * extract original sender from Reply-To: or From:
     
    11381226                                                }
    11391227                                                if( forward ) {
     1228                                                        // TODO attachments are not forwarded
    11401229                                                        sessionObject.subject = mail.subject;
    11411230                                                        if (!(sessionObject.subject.startsWith("Fwd:") ||
     
    11991288                        else if (request.getParameter(SHOW) != null)
    12001289                                state = State.SHOW;
     1290                        else if (request.getParameter(NEW_UIDL) != null)
     1291                                state = State.NEW;
    12011292                        else
    12021293                                state = State.LIST;
     
    13161407                                OutputStream out = null;
    13171408                                I2PAppContext ctx = I2PAppContext.getGlobalContext();
    1318                                 File f = new File(ctx.getTempDir(), "susimail-attachment-" + ctx.random().nextLong());
     1409                                String temp = "susimail-attachment-" + ctx.random().nextLong();
     1410                                File f;
     1411                                MailCache drafts = sessionObject.caches.get(DIR_DRAFTS);
     1412                                if (drafts != null) {
     1413                                        // preferably save across restarts
     1414                                        f = new File(drafts.getAttachmentDir(), temp);
     1415                                } else {
     1416                                        f = new File(ctx.getTempDir(), temp);
     1417                                }
    13191418                                try {
    13201419                                        in = request.getInputStream( NEW_FILENAME );
     
    14001499                }
    14011500                if( buttonPressed( request, REALLYDELETE ) ) {
    1402                         sessionObject.mailCache.delete(showUIDL);
    1403                         sessionObject.folder.removeElement(showUIDL);
     1501                        MailCache mc = getCurrentMailCache(sessionObject, request);
     1502                        if (mc != null) {
     1503                                mc.delete(showUIDL);
     1504                                mc.getFolder().removeElement(showUIDL);
     1505                        }
    14041506                        return null;
     1507                }
     1508                if (buttonPressed(request, MOVE_TO)) {
     1509                        String uidl = Base64.decodeToString(request.getParameter(B64UIDL));
     1510                        String from = request.getParameter(CURRENT_FOLDER);
     1511                        String to = request.getParameter(NEW_FOLDER);
     1512                        if (uidl != null && from != null && to != null) {
     1513                                MailCache fromMC = sessionObject.caches.get(from);
     1514                                MailCache toMC = sessionObject.caches.get(to);
     1515                                if (fromMC != null && toMC != null) {
     1516                                        waitForLoad(sessionObject, fromMC);
     1517                                        waitForLoad(sessionObject, toMC);
     1518                                        if (fromMC.moveTo(uidl, toMC)) {
     1519                                                // success
     1520                                                return null;
     1521                                        }
     1522                                }
     1523                        }
     1524                        sessionObject.error += "Failed move to " + to + '\n';
    14051525                }
    14061526                return showUIDL;
     
    14251545                        try {
    14261546                                int id = Integer.parseInt( str );
    1427                                 Mail mail = sessionObject.mailCache.getMail(showUIDL, MailCache.FetchMode.ALL);
     1547                                MailCache mc = getCurrentMailCache(sessionObject, request);
     1548                                Mail mail = (mc != null) ? mc.getMail(showUIDL, MailCache.FetchMode.ALL) : null;
    14281549                                MailPart part = mail != null ? getMailPartFromID(mail.getPart(), id) : null;
    14291550                                if( part != null ) {
     
    14561577                if( str == null )
    14571578                        return false;
    1458                 Mail mail = sessionObject.mailCache.getMail(showUIDL, MailCache.FetchMode.ALL);
     1579                MailCache mc = getCurrentMailCache(sessionObject, request);
     1580                Mail mail = (mc != null) ? mc.getMail(showUIDL, MailCache.FetchMode.ALL) : null;
    14591581                if( mail != null ) {
    14601582                        if (sendMailSaveAs(sessionObject, mail, response))
     
    15411663                else if( buttonPressed( request, LASTPAGE ) ) {
    15421664                        sessionObject.pageChanged = true;
    1543                         page = sessionObject.folder.getPages();
     1665                        Folder<String> folder = getCurrentFolder(sessionObject, request);
     1666                        page = (folder != null) ? folder.getPages() : 1;
    15441667                }
    15451668
     
    15641687                                int numberDeleted = toDelete.size();
    15651688                                if (numberDeleted > 0) {
    1566                                         sessionObject.mailCache.delete(toDelete);
    1567                                         sessionObject.folder.removeElements(toDelete);
    1568                                         sessionObject.pageChanged = true;
    1569                                         sessionObject.info += ngettext("1 message deleted.", "{0} messages deleted.", numberDeleted);
     1689                                        MailCache mc = getCurrentMailCache(sessionObject, request);
     1690                                        if (mc != null) {
     1691                                                mc.delete(toDelete);
     1692                                                mc.getFolder().removeElements(toDelete);
     1693                                                sessionObject.pageChanged = true;
     1694                                                sessionObject.info += ngettext("1 message deleted.", "{0} messages deleted.", numberDeleted);
     1695                                        }
    15701696                                        //sessionObject.error += _t("Error deleting message: {0}", sessionObject.mailbox.lastError()) + '\n';
    15711697                                }
     
    16001726                        }
    16011727                        // Store in session. processRequest() will re-sort if necessary.
    1602                         sessionObject.folder.setSortBy(str, order);
     1728                        Folder<String> folder = getCurrentFolder(sessionObject, request);
     1729                        if (folder != null)
     1730                                folder.setSortBy(str, order);
    16031731                }
    16041732        }
     
    16341762                                }
    16351763                                String ps = props.getProperty(Folder.PAGESIZE);
    1636                                 if (sessionObject.folder != null && ps != null) {
     1764                                Folder<String> folder = getCurrentFolder(sessionObject, request);
     1765                                if (folder != null && ps != null) {
    16371766                                        try {
    16381767                                                int pageSize = Math.max(5, Integer.parseInt(request.getParameter(PAGESIZE)));
    1639                                                 int oldPageSize = sessionObject.folder.getPageSize();
     1768                                                int oldPageSize = folder.getPageSize();
    16401769                                                if( pageSize != oldPageSize )
    1641                                                         sessionObject.folder.setPageSize( pageSize );
     1770                                                        folder.setPageSize( pageSize );
    16421771                                        } catch( NumberFormatException nfe ) {}
    16431772                                }
     
    16451774                                boolean release = !Boolean.parseBoolean(props.getProperty(CONFIG_DEBUG));
    16461775                                Debug.setLevel( release ? Debug.ERROR : Debug.DEBUG );
    1647                                 state = sessionObject.folder != null ? State.LIST : State.AUTH;
     1776                                state = folder != null ? State.LIST : State.AUTH;
    16481777                                sessionObject.info = _t("Configuration saved");
    16491778                        } catch (IOException ioe) {
     
    16511780                        }
    16521781                } else if (buttonPressed(request, SETPAGESIZE)) {
     1782                        Folder<String> folder = getCurrentFolder(sessionObject, request);
    16531783                        try {
    16541784                                int pageSize = Math.max(5, Integer.parseInt(request.getParameter(PAGESIZE)));
    1655                                 if (sessionObject.folder != null) {
    1656                                         int oldPageSize = sessionObject.folder.getPageSize();
     1785                                if (folder != null) {
     1786                                        int oldPageSize = folder.getPageSize();
    16571787                                        if( pageSize != oldPageSize )
    1658                                                 sessionObject.folder.setPageSize( pageSize );
     1788                                                folder.setPageSize( pageSize );
    16591789                                        state = State.LIST;
    16601790                                } else {
     
    16701800                        }
    16711801                } else if (buttonPressed(request, CANCEL)) {
    1672                         state = (sessionObject.folder != null) ? State.LIST : State.AUTH;
     1802                        Folder<String> folder = getCurrentFolder(sessionObject, request);
     1803                        state = (folder != null) ? State.LIST : State.AUTH;
    16731804                }
    16741805                return state;
     
    17071838        return ServletUtil.isSmallBrowser(ua);
    17081839    }
     1840
     1841        /**
     1842         *  @return folder or null
     1843         *  @since 0.9.35
     1844         */
     1845        private static MailCache getCurrentMailCache(SessionObject session, RequestWrapper request) {
     1846                String folderName = (buttonPressed(request, SWITCH_TO) || buttonPressed(request, MOVE_TO)) ?
     1847                                    request.getParameter(NEW_FOLDER) : null;
     1848                if (folderName == null) {
     1849                        if (buttonPressed(request, SAVE_AS_DRAFT) || buttonPressed(request, NEW_UIDL) ||
     1850                            (buttonPressed(request, CANCEL) && request.getParameter(NEW_SUBJECT) != null)) {
     1851                                folderName = DIR_DRAFTS;
     1852                        } else {
     1853                                folderName = request.getParameter(CURRENT_FOLDER);
     1854                                if (folderName == null)
     1855                                        folderName = DIR_FOLDER;
     1856                        }
     1857                }
     1858                MailCache rv = session.caches.get(folderName);
     1859                if (rv == null) {
     1860                        // only show error if logged in
     1861                        if (DIR_FOLDER.equals(folderName)) {
     1862                                if (session.user != null)
     1863                                        session.error += "Cannot load Inbox\n";
     1864                        } else {
     1865                                if (session.user != null)
     1866                                        session.error += "Folder not found: " + folderName + '\n';
     1867                                rv = session.caches.get(DIR_FOLDER);
     1868                                if (rv == null && session.user != null)
     1869                                        session.error += "Cannot load Inbox\n";
     1870                        }
     1871                }
     1872                return rv;
     1873        }
     1874
     1875        /**
     1876         *  @return folder or null
     1877         *  @since 0.9.35
     1878         */
     1879        private static Folder<String> getCurrentFolder(SessionObject session, RequestWrapper request) {
     1880                MailCache mc = getCurrentMailCache(session, request);
     1881                return (mc != null) ? mc.getFolder() : null;
     1882        }
     1883
     1884        /**
     1885         *  Blocking wait
     1886         *  @since 0.9.35
     1887         */
     1888        private static void waitForLoad(SessionObject sessionObject, MailCache mc) {
     1889                if (!mc.isLoaded()) {
     1890                        boolean ok = true;
     1891                        if (!mc.isLoading())
     1892                                ok = mc.loadFromDisk(new LoadWaiter(sessionObject, mc));
     1893                        if (ok) {
     1894                                while (mc.isLoading()) {
     1895                                        try {
     1896                                                sessionObject.wait(5000);
     1897                                        } catch (InterruptedException ie) {
     1898                                                Debug.debug(Debug.DEBUG, "Interrupted waiting for load", ie);
     1899                                        }
     1900                                }
     1901                        }
     1902                }
     1903        }
    17091904
    17101905        //// Start request handling here ////
     
    17651960                synchronized( sessionObject ) {
    17661961                       
    1767                         sessionObject.error = "";
    1768                         sessionObject.info = "";
    17691962                        sessionObject.pageChanged = false;
    17701963                        sessionObject.themePath = "/themes/susimail/" + theme + '/';
     
    17731966                       
    17741967                        if (isPOST) {
     1968                                // TODO not perfect, but only clear on POST so they survive a P-R-G
     1969                                sessionObject.error = "";
     1970                                sessionObject.info = "";
    17751971                                try {
    17761972                                        String nonce = request.getParameter(SUSI_NONCE);
     
    18172013                        if (state == State.LOADING) {
    18182014                                if (isPOST) {
    1819                                         sendRedirect(httpRequest, response, null);
     2015                                        String q = null;
     2016                                        if (buttonPressed(request, SAVE_AS_DRAFT)) {
     2017                                                q = '?' + CURRENT_FOLDER + '=' + DIR_DRAFTS;
     2018                                        } else {
     2019                                                String str = request.getParameter(NEW_FOLDER);
     2020                                                if (str == null) {
     2021                                                        str = request.getParameter(CURRENT_FOLDER);
     2022                                                        if (str != null && !str.equals(DIR_FOLDER) &&
     2023                                                            (buttonPressed(request, SWITCH_TO) || buttonPressed(request, MOVE_TO)))
     2024                                                                q = '?' + CURRENT_FOLDER + '=' + str;
     2025                                                }
     2026                                        }
     2027                                        sendRedirect(httpRequest, response, q);
    18202028                                        return;
    18212029                                }
     
    18512059                                            buttonPressed(request, SEND) || buttonPressed(request, CANCEL) ||
    18522060                                            buttonPressed(request, REFRESH) ||
     2061                                            buttonPressed(request, SWITCH_TO) || buttonPressed(request, MOVE_TO) ||
    18532062                                            buttonPressed(request, LOGIN) || buttonPressed(request, OFFLINE)) {
    18542063                                                // P-R-G
     
    18572066                                                String str = request.getParameter(CURRENT_SORT);
    18582067                                                if (str != null && !str.equals(SORT_DEFAULT) && VALID_SORTS.contains(str))
    1859                                                         q += "&sort=" + str;
     2068                                                        q += '&' + SORT + '=' + str;
     2069                                                str = null;
     2070                                                if (buttonPressed(request, SWITCH_TO) || buttonPressed(request, MOVE_TO))
     2071                                                        str = request.getParameter(NEW_FOLDER);
     2072                                                if (str == null)
     2073                                                        str = request.getParameter(CURRENT_FOLDER);
     2074                                                if (str != null && !str.equals(DIR_FOLDER))
     2075                                                        q += '&' + CURRENT_FOLDER + '=' + str;
    18602076                                                sendRedirect(httpRequest, response, q);
    18612077                                                return;
     
    18632079                                }
    18642080                        }
    1865                        
     2081
     2082                        if (state == State.NEW) {
     2083                                if (isPOST) {
     2084                                        String q = '?' + NEW_UIDL;
     2085                                        String newUIDL = request.getParameter(NEW_UIDL);
     2086                                        if (newUIDL != null)
     2087                                                q += '=' + newUIDL;
     2088                                        sendRedirect(httpRequest, response, q);
     2089                                        return;
     2090                                }
     2091                        }
     2092
    18662093                        // ?show= links - this forces State.SHOW
    18672094                        String b64UIDL = request.getParameter(SHOW);
     
    18702097                                b64UIDL = request.getParameter(B64UIDL);
    18712098                        String showUIDL = Base64.decodeToString(b64UIDL);
    1872                         if( state == State.SHOW ) {
     2099                        if (state == State.SHOW) {
    18732100                                if (isPOST) {
    18742101                                        String newShowUIDL = processMessageButtons(sessionObject, showUIDL, request);
     
    18852112                                                else
    18862113                                                        q = '?' + SHOW + '=' + Base64.encode(newShowUIDL);
     2114                                                String str = request.getParameter(CURRENT_FOLDER);
     2115                                                if (str != null && !str.equals(DIR_FOLDER))
     2116                                                        q += '&' + CURRENT_FOLDER + '=' + str;
    18872117                                                sendRedirect(httpRequest, response, q);
    18882118                                                return;
     
    19022132                                // sessionObject.showUIDL = null
    19032133                                if (showUIDL != null) {
    1904                                         Mail mail = sessionObject.mailCache.getMail(showUIDL, MailCache.FetchMode.ALL);
     2134                                        MailCache mc = getCurrentMailCache(sessionObject, request);
     2135                                        Mail mail = (mc != null) ? mc.getMail(showUIDL, MailCache.FetchMode.ALL) : null;
    19052136                                        if( mail != null && mail.error.length() > 0 ) {
    19062137                                                sessionObject.error += mail.error;
     
    19172148                         * We need a valid and sorted folder for SHOW also, for the previous/next buttons
    19182149                         */
    1919                         Folder<String> folder = sessionObject.folder;
     2150                        MailCache mc = getCurrentMailCache(sessionObject, request);
    19202151                        // folder could be null after an error, we can't proceed if it is
    1921                         if (folder == null && (state == State.LIST || state == State.SHOW)) {
     2152                        if (mc == null && (state == State.LIST || state == State.SHOW || state == State.NEW)) {
    19222153                                sessionObject.error += "Internal error, no folder\n";
    19232154                                state = State.AUTH;
    1924                         }
     2155                        } else if (mc != null) {
     2156                                if (!mc.isLoaded() && !mc.isLoading()) {
     2157                                        boolean ok = mc.loadFromDisk(new LoadWaiter(sessionObject, mc));
     2158                                        // wait a little while so we avoid the loading page if we can
     2159                                        if (ok) {
     2160                                                try {
     2161                                                        sessionObject.wait(5000);
     2162                                                } catch (InterruptedException ie) {
     2163                                                        Debug.debug(Debug.DEBUG, "Interrupted waiting for load", ie);
     2164                                                }
     2165                                        }
     2166                                        if ((state == State.LIST || state == State.SHOW) && mc.isLoading())
     2167                                            state = State.LOADING;
     2168                                }
     2169                        }
     2170                        Folder<String> folder = mc != null ? mc.getFolder() : null;
    19252171
    19262172                        //// End state determination, state will not change after here
     
    19282174
    19292175                        if (state == State.LIST || state == State.SHOW) {
     2176                                // mc non-null
    19302177                                // sort buttons are GETs
    19312178                                String oldSort = folder.getCurrentSortBy();
     
    19332180                                processSortingButtons( sessionObject, request );
    19342181                                if (state == State.LIST) {
    1935                                         for (Iterator<String> it = sessionObject.folder.currentPageIterator(); it != null && it.hasNext(); ) {
     2182                                        for (Iterator<String> it = folder.currentPageIterator(); it != null && it.hasNext(); ) {
    19362183                                                String uidl = it.next();
    1937                                                 Mail mail = sessionObject.mailCache.getMail( uidl, MailCache.FetchMode.HEADER );
     2184                                                Mail mail = mc.getMail(uidl, MailCache.FetchMode.HEADER);
    19382185                                                if( mail != null && mail.error.length() > 0 ) {
    19392186                                                        sessionObject.error += mail.error;
     
    19442191
    19452192                                // get through cache so we have the disk-only ones too
    1946                                 String[] uidls = sessionObject.mailCache.getUIDLs();
     2193                                String[] uidls = mc.getUIDLs();
    19472194                                if (folder.addElements(Arrays.asList(uidls)) > 0) {
    19482195                                        // we added elements, so it got sorted
     
    19742221                                        // Not only does it slow things down, but a failure causes all our messages to "vanish"
    19752222                                        //subtitle = ngettext("1 Message", "{0} Messages", sessionObject.mailbox.getNumMails());
    1976                                         subtitle = ngettext("1 Message", "{0} Messages", folder.getSize());
     2223                                        int sz = folder.getSize();
     2224                                        subtitle = mc.getTranslatedName() + " - ";
     2225                                        if (sz > 0)
     2226                                                subtitle += ngettext("1 Message", "{0} Messages", folder.getSize());
     2227                                        else
     2228                                                subtitle += _t("No messages");
    19772229                                } else if( state == State.SHOW ) {
    1978                                         Mail mail = showUIDL != null ? sessionObject.mailCache.getMail(showUIDL, MailCache.FetchMode.HEADER) : null;
     2230                                        // mc non-null
     2231                                        Mail mail = showUIDL != null ? mc.getMail(showUIDL, MailCache.FetchMode.HEADER) : null;
    19792232                                        if (mail != null && mail.hasHeader()) {
    19802233                                                if (mail.shortSubject != null)
     
    20272280                                        // we use this to know if the user thought he was logged in at the time
    20282281                                        "<input type=\"hidden\" name=\"" + DEBUG_STATE + "\" value=\"" + state + "\">");
     2282                                if (state == State.NEW) {
     2283                                        String newUIDL = request.getParameter(NEW_UIDL);
     2284                                        if (newUIDL == null || newUIDL.length() <= 0)
     2285                                                newUIDL = Base64.encode(ctx.random().nextLong() + "drft");
     2286                                        out.println("<input type=\"hidden\" name=\"" + NEW_UIDL + "\" value=\"" + newUIDL + "\">");
     2287                                }
    20292288                                if( state == State.SHOW || state == State.NEW) {
    20302289                                        // Store the reference UIDL on the compose form also
     
    20522311                                        String fullSort = curOrder == SortOrder.UP ? '-' + curSort : curSort;
    20532312                                        out.println("<input type=\"hidden\" name=\"" + CURRENT_SORT + "\" value=\"" + fullSort + "\">");
    2054                                         out.println("<input type=\"hidden\" name=\"" + CURRENT_FOLDER + "\" value=\"" + PersistentMailCache.DIR_FOLDER + "\">");
     2313                                        out.println("<input type=\"hidden\" name=\"" + CURRENT_FOLDER + "\" value=\"" + mc.getFolderName() + "\">");
    20552314                                }
    20562315                                boolean showRefresh = false;
    2057                                 if (sessionObject.isLoading) {
    2058                                         sessionObject.info += _t("Loading emails, please wait...") + '\n';
     2316                                if (mc != null && mc.isLoading()) {
     2317                                        // not += so it doesn't cascade
     2318                                        sessionObject.info = _t("Loading emails, please wait...") + '\n';
     2319                                        if (sessionObject.isFetching)
     2320                                                sessionObject.info += _t("Checking for new emails on server") + '\n';
    20592321                                        showRefresh = true;
    2060                                 }
    2061                                 if (sessionObject.isFetching) {
    2062                                         sessionObject.info += _t("Checking for new emails on server") + '\n';
     2322                                } else if (sessionObject.isFetching) {
     2323                                        // not += so it doesn't cascade
     2324                                        sessionObject.info = _t("Checking for new emails on server") + '\n';
    20632325                                        showRefresh = true;
    20642326                                } else if (state != State.LOADING && state != State.AUTH && state != State.CONFIG) {
     
    20982360                               
    20992361                                else if( state == State.LIST )
    2100                                         showFolder( out, sessionObject, request );
     2362                                        showFolder( out, sessionObject, mc, request );
    21012363                               
    21022364                                else if( state == State.SHOW )
    2103                                         showMessage(out, sessionObject, showUIDL, buttonPressed(request, DELETE));
     2365                                        showMessage(out, sessionObject, mc, showUIDL, buttonPressed(request, DELETE));
    21042366                               
    21052367                                else if( state == State.NEW )
     
    21072369                               
    21082370                                else if( state == State.CONFIG )
    2109                                         showConfig(out, sessionObject);
     2371                                        showConfig(out, folder);
    21102372                               
    21112373                                //out.println( "</form><div id=\"footer\"><hr><p class=\"footer\">susimail v0." + version +" " + ( RELEASE ? "release" : "development" ) + " &copy; 2004-2005 <a href=\"mailto:susi23@mail.i2p\">susi</a></div></div></body>\n</html>");                               
     
    22472509
    22482510        /**
    2249          * @param sessionObject
     2511         * Take the data from the request, and put it in a StringBuilder
     2512         * suitable for writing out as a Draft.
     2513         *
     2514         * We do no validation of recipients, total length, sender, etc. here.
     2515         *
     2516         * @param sessionObject only for error messages. All data is in the request.
    22502517         * @param request
    2251          * @return success
    2252          */
    2253         private static boolean sendMail( SessionObject sessionObject, RequestWrapper request )
    2254         {
     2518         * @return null on error
     2519         */
     2520        private static StringBuilder composeDraft(SessionObject sessionObject, RequestWrapper request) {
    22552521                boolean ok = true;
    22562522               
     
    22592525                String cc = request.getParameter( NEW_CC );
    22602526                String bcc = request.getParameter( NEW_BCC );
    2261                 String subject = request.getParameter( NEW_SUBJECT, _t("no subject") );
     2527                String subject = request.getParameter(NEW_SUBJECT);
     2528                if (subject == null || subject.trim().length() <= 0)
     2529                    subject = _t("no subject");
     2530                else
     2531                    subject = subject.trim();
    22622532                String text = request.getParameter( NEW_TEXT, "" );
    22632533
     
    22672537                        from = "<" + sessionObject.user + "@" + domain + ">";
    22682538                }
     2539                ArrayList<String> toList = new ArrayList<String>();
     2540                ArrayList<String> ccList = new ArrayList<String>();
     2541                ArrayList<String> bccList = new ArrayList<String>();
     2542               
     2543                String sender = null;
     2544                if (from != null && Mail.validateAddress(from)) {
     2545                        sender = Mail.getAddress( from );
     2546                }
     2547               
     2548                // no validation
     2549                Mail.getRecipientsFromList( toList, to, ok );
     2550                Mail.getRecipientsFromList( ccList, cc, ok );
     2551                Mail.getRecipientsFromList( bccList, bcc, ok );
     2552               
     2553                String bccToSelf = request.getParameter( NEW_BCC_TO_SELF );
     2554                boolean toSelf = "1".equals(bccToSelf);
     2555                // save preference in session
     2556                sessionObject.bccToSelf = toSelf;
     2557                if (toSelf && sender != null && sender.length() > 0 && !bccList.contains(sender))
     2558                        bccList.add( sender );
     2559               
     2560                Encoding qp = EncodingFactory.getEncoding( "quoted-printable" );
     2561                Encoding hl = EncodingFactory.getEncoding( "HEADERLINE" );
     2562
     2563                StringBuilder body = null;
     2564                if( ok ) {
     2565                        boolean multipart = sessionObject.attachments != null && !sessionObject.attachments.isEmpty();
     2566                        if (multipart) {
     2567                                // use Draft just to write out attachment headers
     2568                                Draft draft = new Draft("");
     2569                                for(Attachment a : sessionObject.attachments) {
     2570                                        draft.addAttachment(a);
     2571                                }
     2572                                body = draft.encodeAttachments();
     2573                        } else {
     2574                                body = new StringBuilder(1024);
     2575                        }
     2576                        I2PAppContext ctx = I2PAppContext.getGlobalContext();
     2577                        body.append("Date: " + RFC822Date.to822Date(ctx.clock().now()) + "\r\n");
     2578                        // todo include real names, and headerline encode them
     2579                        if (from != null)
     2580                                body.append( "From: " + from + "\r\n" );
     2581                        Mail.appendRecipients( body, toList, "To: " );
     2582                        Mail.appendRecipients( body, ccList, "Cc: " );
     2583                        // only for draft
     2584                        Mail.appendRecipients(body, bccList, Draft.HDR_BCC);
     2585                        try {
     2586                                body.append(hl.encode("Subject: " + subject));
     2587                        } catch (EncodingException e) {
     2588                                ok = false;
     2589                                sessionObject.error += e.getMessage() + '\n';
     2590                                Debug.debug(Debug.DEBUG, "Draft subj", e);
     2591                        }
     2592                        body.append("MIME-Version: 1.0\r\nContent-type: text/plain; charset=\"utf-8\"\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\n");
     2593                        try {
     2594                                body.append(qp.encode(text));
     2595                                int len = body.length();
     2596                                if (body.charAt(len - 2) != '\r' && body.charAt(len - 1) != '\n')
     2597                                        body.append("\r\n");
     2598                        } catch (EncodingException e) {
     2599                                ok = false;
     2600                                sessionObject.error += e.getMessage() + '\n';
     2601                                Debug.debug(Debug.DEBUG, "Draft body", e);
     2602                        }
     2603                }                       
     2604                return ok ? body : null;       
     2605        }                       
     2606
     2607        /**
     2608         * Take the data from the request, and put it in a StringBuilder
     2609         * suitable for writing out as a Draft.
     2610         *
     2611         * @param sessionObject only for error messages. All data is in the draft.
     2612         * @return success
     2613         */
     2614        private static boolean sendMail(SessionObject sessionObject, Draft draft) {
     2615                boolean ok = true;
     2616               
     2617                String from = draft.sender;
     2618                String[] to = draft.to;
     2619                String[] cc = draft.cc;
     2620                String[] bcc = draft.getBcc();
     2621                String subject = draft.subject;
     2622                MailPart text = draft.getPart();
     2623                List<Attachment> attachments = draft.getAttachments();
     2624
    22692625                ArrayList<String> toList = new ArrayList<String>();
    22702626                ArrayList<String> ccList = new ArrayList<String>();
     
    22942650                recipients.addAll( bccList );
    22952651               
    2296                 String bccToSelf = request.getParameter( NEW_BCC_TO_SELF );
    2297                 boolean toSelf = "1".equals(bccToSelf);
    2298                 // save preference in session
    2299                 sessionObject.bccToSelf = toSelf;
    2300                 if (toSelf)
    2301                         recipients.add( sender );
    2302                
    23032652                if( toList.isEmpty() ) {
    23042653                        ok = false;
    23052654                        sessionObject.error += _t("No recipients found.") + '\n';
    23062655                }
    2307                 Encoding qp = EncodingFactory.getEncoding( "quoted-printable" );
    23082656                Encoding hl = EncodingFactory.getEncoding( "HEADERLINE" );
    2309                
    2310                 if( qp == null ) {
    2311                         ok = false;
    2312                         // can't happen, don't translate
    2313                         sessionObject.error += "Internal error: Quoted printable encoder not available.";
    2314                 }
    2315                
    2316                 if( hl == null ) {
    2317                         ok = false;
    2318                         // can't happen, don't translate
    2319                         sessionObject.error += "Internal error: Header line encoder not available.";
    2320                 }
    2321 
    2322                 long total = text.length();
    2323                 boolean multipart = sessionObject.attachments != null && !sessionObject.attachments.isEmpty();
     2657
     2658                // Not perfectly accurate but close
     2659                long total = draft.getSize();
     2660                boolean multipart = attachments != null && !attachments.isEmpty();
    23242661                if (multipart) {
    2325                         for(Attachment a : sessionObject.attachments) {
     2662                        for(Attachment a : attachments) {
    23262663                                total += a.getSize();
    23272664                        }
     
    23472684                                sessionObject.error += e.getMessage();
    23482685                        }
     2686
    23492687                        String boundary = "_=" + ctx.random().nextLong();
    23502688                        if (multipart) {
     
    23542692                                body.append( "MIME-Version: 1.0\r\nContent-type: text/plain; charset=\"utf-8\"\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\n" );
    23552693                        }
     2694                        // TODO pass the text separately to SMTP and let it pick the encoding
     2695                        if( multipart )
     2696                                body.append( "--" + boundary + "\r\nContent-type: text/plain; charset=\"utf-8\"\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\n" );
    23562697                        try {
    2357                                 // TODO pass the text separately to SMTP and let it pick the encoding
    2358                                 if( multipart )
    2359                                         body.append( "--" + boundary + "\r\nContent-type: text/plain; charset=\"utf-8\"\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\n" );
    2360                                 body.append( qp.encode( text ) );
    2361                         } catch (EncodingException e) {
     2698                                ByteArrayOutputStream baos = new ByteArrayOutputStream(4096);
     2699                                draft.getPart().outputRaw(baos);
     2700                                body.append(new String(baos.toByteArray(), "ISO-8859-1"));
     2701                        } catch (IOException ioe) {
    23622702                                ok = false;
    2363                                 sessionObject.error += e.getMessage();
    2364                         }
    2365 
    2366                         // set to the StringBuilder so SMTP can replace() in place
    2367                         sessionObject.sentMail = body; 
    2368                        
     2703                                sessionObject.error += ioe.getMessage() + '\n';
     2704                        }
     2705
    23692706                        if( ok ) {
    2370                                 SMTPClient relay = new SMTPClient();
    2371                                 if( relay.sendMail( sessionObject.host, sessionObject.smtpPort,
    2372                                                 sessionObject.user, sessionObject.pass,
    2373                                                 sender, recipients.toArray(new String[recipients.size()]), sessionObject.sentMail,
    2374                                                 sessionObject.attachments, boundary)) {
    2375                                         sessionObject.info += _t("Mail sent.");
    2376                                         sessionObject.sentMail = null; 
    2377                                         sessionObject.clearAttachments();
    2378                                 }
    2379                                 else {
    2380                                                 ok = false;
    2381                                                 sessionObject.error += relay.error;
    2382                                 }
    2383                         }
    2384                 }
     2707                                Runnable es = new EmailSender(sessionObject, draft, sender,
     2708                                                              recipients.toArray(new String[recipients.size()]),
     2709                                                              body, attachments, boundary);
     2710                                Thread t = new I2PAppThread(es, "Email fetcher");
     2711                                sessionObject.info += _t("Sending mail.") + '\n';
     2712                                t.start();
     2713                        }
     2714                }
     2715                if (!ok)
     2716                        sessionObject.error = _t("Error sending mail") + '\n' + sessionObject.error;
    23852717                return ok;
     2718        }
     2719
     2720        /**
     2721         * Threaded sending
     2722         * @since 0.9.35
     2723         */
     2724        private static class EmailSender implements Runnable {
     2725                private final SessionObject sessionObject;
     2726                private final Draft draft;
     2727                private final String host, user, pass, sender, boundary;
     2728                private final int port;
     2729                private final String[] recipients;
     2730                private final StringBuilder body;
     2731                private final List<Attachment> attachments;
     2732
     2733                public EmailSender(SessionObject so, Draft d, String s, String[] recip,
     2734                                   StringBuilder bod, List<Attachment> att, String b) {
     2735                        sessionObject = so; draft = d;
     2736                        host = so.host; port = so.smtpPort; user = so.user; pass = so.pass;
     2737                        sender = s; boundary = b;
     2738                        recipients = recip; body = bod; attachments = att;
     2739                }
     2740
     2741                public void run() {
     2742                        Debug.debug(Debug.DEBUG, "Email send start");
     2743                        SMTPClient relay = new SMTPClient();
     2744                        boolean ok = relay.sendMail(host, port, user, pass, sender, recipients, body,
     2745                                                    attachments, boundary);
     2746                        Debug.debug(Debug.DEBUG, "Email send complete, success? " + ok);
     2747                        synchronized(sessionObject) {
     2748                                if (ok) {
     2749                                        sessionObject.info += _t("Mail sent.") + '\n';
     2750                                        // now delete from drafts
     2751                                        draft.clearAttachments();
     2752                                        MailCache mc = sessionObject.caches.get(DIR_DRAFTS);
     2753                                        if (mc != null) {
     2754                                                waitForLoad(sessionObject, mc);
     2755                                                mc.delete(draft.uidl);
     2756                                                mc.getFolder().removeElement(draft.uidl);
     2757                                                Debug.debug(Debug.DEBUG, "Sent email deleted from drafts");
     2758                                        }
     2759                                        // now store to sent
     2760                                        mc = sessionObject.caches.get(DIR_SENT);
     2761                                        if (mc != null) {
     2762                                                waitForLoad(sessionObject, mc);
     2763                                                I2PAppContext ctx = I2PAppContext.getGlobalContext();
     2764                                                String uidl = ctx.random().nextLong() + "sent";
     2765                                                Writer wout = null;
     2766                                                boolean copyOK = false;
     2767                                                Buffer buffer = null;
     2768                                                try {
     2769                                                        buffer = mc.getFullWriteBuffer(uidl);
     2770                                                        wout = new BufferedWriter(new OutputStreamWriter(buffer.getOutputStream(), "ISO-8859-1"));
     2771                                                        SMTPClient.writeMail(wout, body,
     2772                                                                             attachments, boundary);
     2773                                                        Debug.debug(Debug.DEBUG, "Sent email saved to Sent");
     2774                                                        copyOK = true;
     2775                                                } catch (IOException ioe) {
     2776                                                        sessionObject.error += _t("Unable to save mail.") + ' ' + ioe.getMessage() + '\n';
     2777                                                        Debug.debug(Debug.DEBUG, "Sent email saved error", ioe);
     2778                                                } finally {
     2779                                                        if (wout != null) try { wout.close(); } catch (IOException ioe) {}
     2780                                                        if (buffer != null)
     2781                                                                mc.writeComplete(uidl, buffer, copyOK);
     2782                                                }
     2783                                        }
     2784                                } else {
     2785                                        sessionObject.error += relay.error;
     2786                                }
     2787                                sessionObject.info.replace(_t("Sending mail.") + '\n', "");
     2788                        }
     2789                }
    23862790        }
    23872791
     
    24072811
    24082812        /**
     2813         * @param arr may be null
     2814         * @since 0.9.35
     2815         */
     2816        private static String arrayToCSV(String[] arr) {
     2817                StringBuilder buf = new StringBuilder(64);
     2818                if (arr != null) {
     2819                        for (int i = 0; i < arr.length; i++) {
     2820                                buf.append(arr[i]);
     2821                                if (i < arr.length - 1)
     2822                                        buf.append(", ");
     2823                        }
     2824                }
     2825                return buf.toString();
     2826        }
     2827
     2828        /**
    24092829         *
    24102830         * @param out
     
    24152835        {
    24162836                out.println("<div class=\"topbuttons\">");
    2417                 out.println( button( SEND, _t("Send") ) + spacer +
    2418                                 button( CANCEL, _t("Cancel") ));
     2837                out.println(button(SEND, _t("Send")) +
     2838                            button(SAVE_AS_DRAFT, _t("Save as Draft")) +
     2839                            button(CANCEL, _t("Cancel")));
    24192840                out.println("</div>");
    24202841                //if (Config.hasConfigFile())
     
    24222843                //out.println(button( LOGOUT, _t("Logout") ) );
    24232844
    2424                 String from = request.getParameter( NEW_FROM );
     2845
     2846                Draft draft = null;
     2847                String from = "";
     2848                String to = "";
     2849                String cc = "";
     2850                String bc = "";
     2851                String bcc = "";
     2852                String subject = "";
     2853                String text = "";
     2854                String b64UIDL = request.getParameter(NEW_UIDL);
     2855                if (b64UIDL == null || b64UIDL.length() <= 0) {
     2856                        // header set in processRequest()
     2857                        I2PAppContext ctx = I2PAppContext.getGlobalContext();
     2858                        b64UIDL = Base64.encode(ctx.random().nextLong() + "drft");
     2859                } else {
     2860                        MailCache drafts = sessionObject.caches.get(DIR_DRAFTS);
     2861                        if (drafts == null) {
     2862                                sessionObject.error += "No Drafts folder?\n";
     2863                                return;
     2864                        }
     2865                        String newUIDL = Base64.decodeToString(b64UIDL);
     2866                        Debug.debug(Debug.DEBUG, "Show draft: " + newUIDL);
     2867                        if (newUIDL != null)
     2868                                draft = (Draft) drafts.getMail(newUIDL, MailCache.FetchMode.CACHE_ONLY);
     2869                        if (draft != null) {
     2870                                // populate from saved draft
     2871                                from = draft.sender;
     2872                                subject = draft.subject;
     2873                                to = arrayToCSV(draft.to);
     2874                                cc = arrayToCSV(draft.cc);
     2875                                bcc = arrayToCSV(draft.getBcc());
     2876                                StringWriter body = new StringWriter(1024);
     2877                                Buffer ob = new OutputStreamBuffer(new DecodingOutputStream(body, "UTF-8"));
     2878                                try {
     2879                                        draft.getPart().decode(0, ob);
     2880                                } catch (IOException ioe) {
     2881                                        sessionObject.error += "Draft decode error: " + ioe.getMessage() + '\n';
     2882                                }
     2883                                text = body.toString();
     2884                                List<Attachment> a = draft.getAttachments();
     2885                                if (!a.isEmpty()) {
     2886                                        if (sessionObject.attachments == null)
     2887                                                sessionObject.attachments = new ArrayList<Attachment>(a.size());
     2888                                        sessionObject.attachments.addAll(a);
     2889                                }
     2890                                // needed when processing the CANCEL button
     2891                                out.println("<input type=\"hidden\" name=\"" + DRAFT_EXISTS + "\" value=\"1\">");
     2892                        }
     2893                }
     2894                if (draft == null) {
     2895                        // populate from session object, as saved in processStateChangeButtons()
     2896                        if (sessionObject.replyTo != null)
     2897                                to = sessionObject.replyTo;
     2898                        if (sessionObject.replyCC != null)
     2899                                cc = sessionObject.replyCC;
     2900                        if (sessionObject.subject != null)
     2901                                subject = sessionObject.subject;
     2902                        if (sessionObject.body != null)
     2903                                text = sessionObject.body;
     2904                }
     2905
    24252906                boolean fixed = Boolean.parseBoolean(Config.getProperty( CONFIG_SENDER_FIXED, "true" ));
    24262907               
    2427                 if (from == null || !fixed) {
     2908                if (from.length() <= 0 || !fixed) {
    24282909                        String user = sessionObject.user;
    24292910                        String name = Config.getProperty(CONFIG_SENDER_NAME);
     
    24472928                }
    24482929               
    2449                 String to = request.getParameter( NEW_TO, sessionObject.replyTo != null ? sessionObject.replyTo : "" );
    2450                 String cc = request.getParameter( NEW_CC, sessionObject.replyCC != null ? sessionObject.replyCC : "" );
    2451                 String bcc = request.getParameter( NEW_BCC, "" );
    2452                 String subject = request.getParameter( NEW_SUBJECT, sessionObject.subject != null ? sessionObject.subject : "" );
    2453                 String text = request.getParameter( NEW_TEXT, sessionObject.body != null ? sessionObject.body : "" );
    24542930                sessionObject.replyTo = null;
    24552931                sessionObject.replyCC = null;
     
    25423018         * @param request
    25433019         */
    2544         private static void showFolder( PrintWriter out, SessionObject sessionObject, RequestWrapper request )
     3020        private static void showFolder( PrintWriter out, SessionObject sessionObject, MailCache mc, RequestWrapper request )
    25453021        {
    25463022                out.println("<div class=\"topbuttons\">");
     
    25523028                        //button( FORWARD, _t("Forward") ) + spacer +
    25533029                        //button( DELETE, _t("Delete") ) + spacer +
    2554                 out.println((sessionObject.isFetching ? button2(REFRESH, _t("Check Mail")) : button(REFRESH, _t("Check Mail"))) + spacer);
     3030                String folderName = mc.getFolderName();
     3031                String floc;
     3032                if (folderName.equals(DIR_FOLDER)) {
     3033                        out.println((sessionObject.isFetching ? button2(REFRESH, _t("Check Mail")) : button(REFRESH, _t("Check Mail"))) + spacer);
     3034                        floc = "";
     3035                } else if (folderName.equals(DIR_DRAFTS)) {
     3036                        floc = "";
     3037                } else {
     3038                        floc = '&' + CURRENT_FOLDER + '=' + folderName;
     3039                }
     3040                boolean isSpamFolder = folderName.equals(DIR_SPAM);
    25553041                //if (Config.hasConfigFile())
    25563042                //      out.println(button( RELOAD, _t("Reload Config") ) + spacer);
    25573043                out.println(button( LOGOUT, _t("Logout") ));
    2558                 Folder<String> folder = sessionObject.folder;
    25593044                int page = 1;
     3045                Folder<String> folder = mc.getFolder();
    25603046                if (folder.getPages() > 1) {
    25613047                        String sp = request.getParameter(CUR_PAGE);
     
    25663052                        }
    25673053                        folder.setCurrentPage(page);
    2568                         showPageButtons(out, page, folder.getPages(), true);
    2569                 }
     3054                }
     3055                showPageButtons(out, folderName, page, folder.getPages(), true);
    25703056                out.println("</div>");
    25713057
     
    25843070                for (Iterator<String> it = folder.currentPageIterator(); it != null && it.hasNext(); ) {
    25853071                        String uidl = it.next();
    2586                         Mail mail = sessionObject.mailCache.getMail(uidl, MailCache.FetchMode.CACHE_ONLY);
     3072                        Mail mail = mc.getMail(uidl, MailCache.FetchMode.CACHE_ONLY);
    25873073                        if (mail == null || !mail.hasHeader()) {
    25883074                                continue;
     
    25933079                        else if (mail.isNew())
    25943080                                type = "linknew";
     3081                        else if (isSpamFolder)
     3082                                type = "linkspam";
    25953083                        else
    25963084                                type = "linkold";
    25973085                        // this is I2P Base64, not the encoder
    25983086                        String b64UIDL = Base64.encode(uidl);
    2599                         String link = "<a href=\"" + myself + "?" + SHOW + "=" + b64UIDL + "\" class=\"" + type + "\">";
    2600                         String jslink = " onclick=\"document.location='" + myself + '?' + SHOW + '=' + b64UIDL + "';\" ";
     3087                        String loc = myself + '?' + (folderName.equals(DIR_DRAFTS) ? NEW_UIDL : SHOW) + '=' + b64UIDL + floc;
     3088                        String link = "<a href=\"" + loc + "\" class=\"" + type + "\">";
     3089                        String jslink = " onclick=\"document.location='" + loc + "';\" ";
    26013090                       
    26023091                        boolean idChecked = false;
     
    26423131                        // show the buttons again if page is big
    26433132                        out.println("<tr class=\"bottombuttons\"><td colspan=\"9\" align=\"center\">");
    2644                         showPageButtons(out, page, folder.getPages(), false);
     3133                        showPageButtons(out, folderName, page, folder.getPages(), false);
    26453134                        out.println("</td></tr>");
    26463135                }
     
    26763165
    26773166        /**
     3167         *  Folder selector, then, if pages greater than 1:
    26783168         *  first prev next last
    26793169         */
    2680         private static void showPageButtons(PrintWriter out, int page, int pages, boolean outputHidden) {
     3170        private static void showPageButtons(PrintWriter out, String folderName, int page, int pages, boolean outputHidden) {
    26813171                out.println("<table id=\"pagenav\"><tr><td>");
    2682                 if (outputHidden)
    2683                         out.println("<input type=\"hidden\" name=\"" + CUR_PAGE + "\" value=\"" + page + "\">");
    2684                 String t1 = _t("First");
    2685                 String t2 = _t("Previous");
    2686                 if (page <= 1) {
    2687                         out.println(button2(FIRSTPAGE, t1) + "&nbsp;" + button2(PREVPAGE, t2));
    2688                 } else {
     3172                String name = folderName.equals(DIR_FOLDER) ? "Inbox" : folderName;
     3173                out.println(_t("Folder") + ": " + _t(name) + "&nbsp;&nbsp;&nbsp;&nbsp;");  // TODO css to center it
     3174                out.println(button(SWITCH_TO, _t("Change to Folder")));
     3175                showFolderSelect(out, folderName, false);
     3176                if (pages > 1) {
    26893177                        if (outputHidden)
    2690                                 out.println("<input type=\"hidden\" name=\"" + PREV_PAGE_NUM + "\" value=\"" + (page - 1) + "\">");
    2691                         out.println(button(FIRSTPAGE, t1) + "&nbsp;" + button(PREVPAGE, t2));
    2692                 }
    2693                 out.println("</td><td>" +
    2694                         _t("Page {0} of {1}", page, pages) +
    2695                         "</td><td>");
    2696                 t1 = _t("Next");
    2697                 t2 = _t("Last");
    2698                 if (page >= pages) {
    2699                         out.println(button2(NEXTPAGE, t1) + "&nbsp;" + button2(LASTPAGE, t2));
    2700                 } else {
    2701                         if (outputHidden)
    2702                                 out.println("<input type=\"hidden\" name=\"" + NEXT_PAGE_NUM + "\" value=\"" + (page + 1) + "\">");
    2703                         out.println(button(NEXTPAGE, t1) + "&nbsp;" + button(LASTPAGE, t2));
     3178                                out.println("<input type=\"hidden\" name=\"" + CUR_PAGE + "\" value=\"" + page + "\">");
     3179                        String t1 = _t("First");
     3180                        String t2 = _t("Previous");
     3181                        if (page <= 1) {
     3182                                out.println(button2(FIRSTPAGE, t1) + "&nbsp;" + button2(PREVPAGE, t2));
     3183                        } else {
     3184                                if (outputHidden)
     3185                                        out.println("<input type=\"hidden\" name=\"" + PREV_PAGE_NUM + "\" value=\"" + (page - 1) + "\">");
     3186                                out.println(button(FIRSTPAGE, t1) + "&nbsp;" + button(PREVPAGE, t2));
     3187                        }
     3188                        out.println("</td><td>" +
     3189                                _t("Page {0} of {1}", page, pages) +
     3190                                "</td><td>");
     3191                        t1 = _t("Next");
     3192                        t2 = _t("Last");
     3193                        if (page >= pages) {
     3194                                out.println(button2(NEXTPAGE, t1) + "&nbsp;" + button2(LASTPAGE, t2));
     3195                        } else {
     3196                                if (outputHidden)
     3197                                        out.println("<input type=\"hidden\" name=\"" + NEXT_PAGE_NUM + "\" value=\"" + (page + 1) + "\">");
     3198                                out.println(button(NEXTPAGE, t1) + "&nbsp;" + button(LASTPAGE, t2));
     3199                        }
    27043200                }
    27053201                out.println("</td></tr></table>");
     3202        }
     3203
     3204        /**
     3205         *  @param disableCurrent true for move to folder, false for select folder
     3206         *  @since 0.9.35
     3207         */
     3208        private static void showFolderSelect(PrintWriter out, String currentName, boolean disableCurrent) {
     3209                out.println("<select name=\"" + NEW_FOLDER + "\">");
     3210                for (int i = 0; i < DIRS.length; i++) {
     3211                        String dir = DIRS[i];
     3212                        if (currentName.equals(dir)) {
     3213                                // can't move or switch to self
     3214                                continue;
     3215                        }
     3216                        if (disableCurrent && DIR_DRAFTS.equals(dir)) {
     3217                                // can't move to drafts
     3218                                continue;
     3219                        }
     3220                        out.print("<option value=\"" + dir + "\" ");
     3221                        if (currentName.equals(dir)) {
     3222                                out.print("selected=\"selected\" ");
     3223                        }
     3224                        out.print('>' + _t(DISPLAY_DIRS[i]));
     3225                        out.println("</option>");
     3226                }
     3227                out.println("</select>");
    27063228        }
    27073229
     
    27123234         * @param reallyDelete was the delete button pushed, if so, show the really delete? message
    27133235         */
    2714         private static void showMessage(PrintWriter out, SessionObject sessionObject, String showUIDL, boolean reallyDelete)
     3236        private static void showMessage(PrintWriter out, SessionObject sessionObject, MailCache mc,
     3237                                        String showUIDL, boolean reallyDelete)
    27153238        {
    27163239                if (reallyDelete) {
     
    27203243                                     "</p>");
    27213244                }
    2722                 Mail mail = sessionObject.mailCache.getMail(showUIDL, MailCache.FetchMode.ALL);
     3245                Mail mail = mc.getMail(showUIDL, MailCache.FetchMode.ALL);
    27233246                if(!RELEASE && mail != null && mail.hasBody() && mail.getSize() < 16384) {
    27243247                        out.println( "<!--" );
     
    27453268                        out.println(button( REPLY, _t("Reply") ) +
    27463269                                button( REPLYALL, _t("Reply All") ) +
    2747                                 button( FORWARD, _t("Forward") ) + spacer +
    2748                                 button( SAVE_AS, _t("Save As") ) + spacer);
     3270                                button( FORWARD, _t("Forward") ) +
     3271                                button( SAVE_AS, _t("Save As")));
     3272                        if (mail.hasBody() && !mc.getFolderName().equals(DIR_DRAFTS)) {
     3273                                // can't move unless has body
     3274                                // can't move from drafts
     3275                                out.println(button(MOVE_TO, _t("Move to Folder")));
     3276                                showFolderSelect(out, mc.getFolderName(), true);
     3277                        }
    27493278                        if (sessionObject.reallyDelete)
    27503279                                out.println(button2(DELETE, _t("Delete")));
     
    27553284                // processRequest() will P-R-G the PREV and NEXT so we have a consistent URL
    27563285                out.println("<div id=\"messagenav\">");
    2757                 Folder<String> folder = sessionObject.folder;
     3286                Folder<String> folder = mc.getFolder();
    27583287                if (hasHeader) {
    27593288                        String uidl = folder.getPreviousElement(showUIDL);
     
    28423371         *  Simple configure page
    28433372         *
     3373         *  @param folder may be null
    28443374         *  @since 0.9.13
    28453375         */
    2846         private static void showConfig(PrintWriter out, SessionObject sessionObject) {
     3376        private static void showConfig(PrintWriter out, Folder<String> folder) {
    28473377                int sz;
    2848                 if (sessionObject.folder != null)
    2849                         sz = sessionObject.folder.getPageSize();
     3378                if (folder != null)
     3379                        sz = folder.getPageSize();
    28503380                else
    28513381                        sz = Config.getProperty(Folder.PAGESIZE, Folder.DEFAULT_PAGESIZE);
     
    28703400                out.println(button(SAVE, _t("Save Configuration")));
    28713401                out.println(button(CANCEL, _t("Cancel")));
    2872                 if (sessionObject.folder != null)
     3402                if (folder != null)
    28733403                        out.println(spacer + button(LOGOUT, _t("Logout") ));
    28743404                out.println("</div>");
     3405        }
     3406
     3407        /** tag for translation */
     3408        private static String _x(String s) {
     3409                return s;
    28753410        }
    28763411
  • apps/susimail/src/src/i2p/susi/webmail/encoding/Encoding.java

    rffad52e r844977c  
    132132        /**
    133133         * This implementation just converts the string to a byte array
    134          * and then calls encode(byte[]).
     134         * and then calls decode(byte[]).
    135135         * Most classes will not need to override.
    136136         *
  • apps/susimail/src/src/i2p/susi/webmail/encoding/HeaderLine.java

    rffad52e r844977c  
    135135         *  such as recipient names on the "To" and "Cc" lines.
    136136         *
    137          *  @param str must start with "field-name: "
     137         *  @param str must start with "field-name: ", must have non-whitespace after that
    138138         */
    139139        @Override
     
    142142                int l = str.indexOf(": ");
    143143                if (l <= 0 || l >= 64)
    144                         throw new EncodingException("bad 'field-name: '" + str);
     144                        throw new EncodingException("bad field-name: " + str);
    145145                l += 2;
    146146                boolean quote = false;
  • apps/susimail/src/src/i2p/susi/webmail/smtp/SMTPClient.java

    rffad52e r844977c  
    222222
    223223        /**
    224          *  @param body without the attachments
     224         *  @param body headers and body, without the attachments
    225225         *  @param attachments may be null
    226226         *  @param boundary non-null if attachments is non-null
     
    249249                                int result = sendCmd(null);
    250250                                if (result != 220) {
     251                                        error += _t("Error sending mail") + '\n';
    251252                                        if (result != 0)
    252253                                                error += _t("Server refused connection") + " (" + result + ")\n";
     
    332333                                // Do it this way so we don't double the memory
    333334                                out = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(), "ISO-8859-1"));
    334                                 out.write(body.toString());
    335                                 // moved from WebMail so we don't bring the attachments into memory
    336                                 // Also TODO use the 250 service extension responses to pick the best encoding
    337                                 // and check the max total size
    338                                 if (attachments != null && !attachments.isEmpty()) {
    339                                         for(Attachment attachment : attachments) {
    340                                                 String encodeTo = attachment.getTransferEncoding();
    341                                                 Encoding encoding = EncodingFactory.getEncoding(encodeTo);
    342                                                 if (encoding == null)
    343                                                         throw new EncodingException( _t("No Encoding found for {0}", encodeTo));
    344                                                 // ref: https://blog.nodemailer.com/2017/01/27/the-mess-that-is-attachment-filenames/
    345                                                 // ref: RFC 2231
    346                                                 // split Content-Disposition into 3 lines to maximize room
    347                                                 // TODO filename*0* for long names...
    348                                                 String name = attachment.getFileName();
    349                                                 String name2 = FilenameUtil.sanitizeFilename(name);
    350                                                 String name3 = FilenameUtil.encodeFilenameRFC5987(name);
    351                                                 out.write("\r\n--" + boundary +
    352                                                           "\r\nContent-type: " + attachment.getContentType() +
    353                                                           "\r\nContent-Disposition: attachment;\r\n\tfilename=\"" + name2 +
    354                                                           "\";\r\n\tfilename*=" + name3 +
    355                                                           "\r\nContent-Transfer-Encoding: " + attachment.getTransferEncoding() +
    356                                                           "\r\n\r\n");
    357                                                 InputStream in = null;
    358                                                 try {
    359                                                         in = attachment.getData();
    360                                                         encoding.encode(in, out);
    361                                                 } finally {
    362                                                         if (in != null) try { in.close(); } catch (IOException ioe) {}
    363                                                 }
    364                                         }
    365                                         out.write( "\r\n--" + boundary + "--\r\n" );
    366                                 }
     335                                writeMail(out, body, attachments, boundary);
    367336                                out.write("\r\n.\r\n");
    368337                                out.flush();
     
    393362
    394363        /**
     364         *  Caller must close out
     365         *
     366         *  @param body headers and body, without the attachments
     367         *  @param attachments may be null
     368         *  @param boundary non-null if attachments is non-null
     369         */
     370        public static void writeMail(Writer out, StringBuilder body,
     371                                     List<Attachment> attachments, String boundary) throws IOException {
     372                out.write(body.toString());
     373                // moved from WebMail so we don't bring the attachments into memory
     374                // Also TODO use the 250 service extension responses to pick the best encoding
     375                // and check the max total size
     376                if (attachments != null && !attachments.isEmpty()) {
     377                        for(Attachment attachment : attachments) {
     378                                String encodeTo = attachment.getTransferEncoding();
     379                                Encoding encoding = EncodingFactory.getEncoding(encodeTo);
     380                                if (encoding == null)
     381                                        throw new EncodingException( _t("No Encoding found for {0}", encodeTo));
     382                                // ref: https://blog.nodemailer.com/2017/01/27/the-mess-that-is-attachment-filenames/
     383                                // ref: RFC 2231
     384                                // split Content-Disposition into 3 lines to maximize room
     385                                // TODO filename*0* for long names...
     386                                String name = attachment.getFileName();
     387                                String name2 = FilenameUtil.sanitizeFilename(name);
     388                                String name3 = FilenameUtil.encodeFilenameRFC5987(name);
     389                                out.write("\r\n--" + boundary +
     390                                          "\r\nContent-type: " + attachment.getContentType() +
     391                                          "\r\nContent-Disposition: attachment;\r\n\tfilename=\"" + name2 +
     392                                          "\";\r\n\tfilename*=" + name3 +
     393                                          "\r\nContent-Transfer-Encoding: " + attachment.getTransferEncoding() +
     394                                          "\r\n\r\n");
     395                                InputStream in = null;
     396                                try {
     397                                        in = attachment.getData();
     398                                        encoding.encode(in, out);
     399                                } finally {
     400                                        if (in != null) try { in.close(); } catch (IOException ioe) {}
     401                                }
     402                        }
     403                        out.write( "\r\n--" + boundary + "--\r\n" );
     404                }
     405        }
     406
     407        /**
    395408         *  A command to send and a result code to expect
    396409         *  @since 0.9.13
  • history.txt

    rffad52e r844977c  
     12018-04-14 zzz
     2 * Console: Add built-by to /logs (ticket #2204)
     3 * CPUID: Fix TBM detection (ticket #2211)
     4 * Debian updates (ticket #2027, PR #15)
     5 * Jetty: Fix quote in header line tripping XSS filter (ticket #2215)
     6 * SusiMail: Add folders, drafts, background sending (ticket #2087)
     7
    182018-04-11 zzz
     9 * Debian updates for 0.9.34
    210 * Jetty 9.2.24-v201801015
    311 * Tomcat 8.5.30
  • router/java/src/net/i2p/router/RouterVersion.java

    rffad52e r844977c  
    1919    public final static String ID = "Monotone";
    2020    public final static String VERSION = CoreVersion.VERSION;
    21     public final static long BUILD = 1;
     21    public final static long BUILD = 2;
    2222
    2323    /** for example "-test" */
Note: See TracChangeset for help on using the changeset viewer.