freemail.OutboundContact.java Source code

Java tutorial

Introduction

Here is the source code for freemail.OutboundContact.java

Source

/*
 * OutboundContact.java
 * This file is part of Freemail
 * Copyright (C) 2006,2007,2008 Dave Baker
 * Copyright (C) 2007,2008 Alexander Lehmann
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 */

package freemail;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.FileNotFoundException;
import java.net.MalformedURLException;
import java.math.BigInteger;
import java.security.SecureRandom;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Set;
import java.util.TreeSet;
import java.io.PrintWriter;

import freemail.utils.EmailAddress;
import freemail.utils.PropsFile;
import freemail.utils.DateStringFactory;
import freemail.fcp.FCPException;
import freemail.fcp.FCPFetchException;
import freemail.fcp.HighLevelFCPClient;
import freemail.fcp.FCPPutFailedException;
import freemail.fcp.FCPBadFileException;
import freemail.fcp.SSKKeyPair;
import freemail.fcp.ConnectionTerminatedException;
import freemail.utils.Logger;

import org.archive.util.Base32;

import org.bouncycastle.crypto.digests.SHA256Digest;
import org.bouncycastle.crypto.params.RSAKeyParameters;
import org.bouncycastle.crypto.AsymmetricBlockCipher;
import org.bouncycastle.crypto.engines.RSAEngine;
import org.bouncycastle.crypto.InvalidCipherTextException;
import org.bouncycastle.crypto.engines.AESEngine;
import org.bouncycastle.crypto.params.KeyParameter;
import org.bouncycastle.crypto.params.ParametersWithIV;
import org.bouncycastle.crypto.paddings.PKCS7Padding;
import org.bouncycastle.crypto.paddings.PaddedBufferedBlockCipher;
import org.bouncycastle.crypto.modes.CBCBlockCipher;
import org.bouncycastle.util.encoders.Base64;

public class OutboundContact {
    public static final String OUTBOX_DIR = "outbox";
    private final PropsFile contactfile;
    private final FreemailAccount account;
    private final File ctoutbox;
    private final EmailAddress address;

    // how long to wait for a CTS before sending the RTS again
    // slightly over 24 hours since some people are likely to fire Freemail
    // up and roughly the same time every day
    private static final long CTS_WAIT_TIME = 26 * 60 * 60 * 1000;

    private static final String PROPSFILE_NAME = "props";

    // How long to wait for a *message ack* before sending another RTS.
    private static final long RTS_RETRANSMIT_DELAY = 2 * 24 * 60 * 60 * 1000;

    // how long do we wait before we give up all hope and just bounce the message back?
    // 5 days is fairly standard, so we'll go with that for now, except that means things
    // bounce when the recipient goes to the Bahamas for a fortnight. Could be longer if
    // we have a GUI to see what messages are in what delivery state.
    private static final long FAIL_DELAY = 5 * 24 * 60 * 60 * 1000;

    private static final int AES_KEY_LENGTH = 256 / 8;

    // this is defined in the AES standard (although the Rijndael
    // algorithm does support other block sizes.
    // we read 128 bytes for our IV, so it needs to be constant.)
    private static final int AES_BLOCK_LENGTH = 128 / 8;

    // If we last fetched the mailsite longer than this number of milliseconds
    // ago, re-fetch it.
    private static final long MAILSITE_CACHE_TIME = 60 * 60 * 1000;

    /**
     * Used to store the index of the next ack we should check. This is done so we won't start from
     * the beginning if we stopped due to a timeout, but instead start where we left of.
     */
    //FIXME: This behaves badly when the outbox changes
    private int nextAckIndex = 0;

    public OutboundContact(FreemailAccount acc, EmailAddress a) throws BadFreemailAddressException, IOException,
            OutboundContactFatalException, ConnectionTerminatedException, InterruptedException {
        this.address = a;

        this.account = acc;

        if (!this.address.is_freemail_address()) {
            this.contactfile = null;
            throw new BadFreemailAddressException();
        } else {
            File contactsdir = new File(account.getAccountDir(), SingleAccountWatcher.CONTACTS_DIR);
            if (!contactsdir.exists()) {
                if (!contactsdir.mkdir()) {
                    throw new IOException("Couldn't create contacts dir!");
                }
            }
            File outbounddir = new File(contactsdir, SingleAccountWatcher.OUTBOUND_DIR);

            if (!outbounddir.exists()) {
                if (!outbounddir.mkdir()) {
                    throw new IOException("Couldn't create outbound dir!");
                }
            }

            if (!this.address.is_ssk_address()) {
                String ssk_mailsite = this.fetchKSKRedirect(this.address.getMailpageKey());

                if (ssk_mailsite == null)
                    throw new IOException();

                FreenetURI furi;
                try {
                    furi = new FreenetURI(ssk_mailsite);
                } catch (MalformedURLException mfue) {
                    throw new OutboundContactFatalException(
                            "The Freemail address points to an invalid redirect, and is therefore useless.");
                }

                this.address.domain = Base32.encode(furi.getKeyBody().getBytes()) + ".freemail";
            }

            File obctdir = new File(outbounddir, this.address.getSubDomain().toLowerCase());

            if (!obctdir.exists() && !obctdir.mkdir()) {
                throw new IOException("Couldn't create outbound contact dir!");
            }

            this.contactfile = PropsFile.createPropsFile(new File(obctdir, PROPSFILE_NAME));
            this.ctoutbox = new File(obctdir, OUTBOX_DIR);
            if (!this.ctoutbox.exists() && !this.ctoutbox.mkdir()) {
                throw new IOException("Couldn't create contact outbox!");
            }
        }
    }

    public OutboundContact(FreemailAccount acc, File ctdir) throws IOException {
        this.account = acc;
        this.address = new EmailAddress();
        this.address.domain = ctdir.getName() + ".freemail";

        this.contactfile = PropsFile.createPropsFile(new File(ctdir, PROPSFILE_NAME));

        this.ctoutbox = new File(ctdir, OUTBOX_DIR);
        if (!this.ctoutbox.exists()) {
            if (!this.ctoutbox.mkdir()) {
                throw new IOException("Couldn't create contact outbox dir!");
            }
        }
    }

    public void checkCTS() throws ConnectionTerminatedException, InterruptedException {
        String status = this.contactfile.get("status");
        if (status == null) {
            this.init();
            status = this.contactfile.get("status");
            if (status == null)
                return;
        }

        if (status.equals("cts-received")) {
            return;
        } else if (status.equals("rts-sent")) {
            // poll for the CTS message

            String ctskey = this.contactfile.get("ackssk.pubkey");
            if (ctskey == null) {
                this.init();
            }
            ctskey += "cts";

            HighLevelFCPClient fcpcli = new HighLevelFCPClient();

            Logger.minor(this, "polling for CTS message: " + ctskey);
            try {
                File cts = fcpcli.fetch(ctskey);

                Logger.normal(this, "Sucessfully received CTS for " + this.address.getSubDomain());
                cts.delete();
                this.contactfile.put("status", "cts-received");
                // delete initial slot for forward secrecy
                this.contactfile.remove("initialslot");
            } catch (FCPFetchException fe) {
                Logger.minor(this, "CTS not received");
                // haven't got the CTS message. should we give up yet?
                String senttime = this.contactfile.get("rts-sent-at");

                if (senttime == null || Long.parseLong(senttime) + CTS_WAIT_TIME < System.currentTimeMillis()) {
                    // yes, send another RTS
                    this.init();
                }
            } catch (FCPException e) {
                Logger.error(this, "Unknown error while checking CTS: " + e);
                //TODO: Should we resend the RTS like above?
            }
        } else {
            this.init();
        }
    }

    private SSKKeyPair getCommKeyPair() throws ConnectionTerminatedException, InterruptedException {
        SSKKeyPair ssk = new SSKKeyPair();

        ssk.pubkey = this.contactfile.get("commssk.pubkey");
        ssk.privkey = this.contactfile.get("commssk.privkey");

        if (ssk.pubkey == null || ssk.privkey == null) {
            HighLevelFCPClient cli = new HighLevelFCPClient();
            ssk = cli.makeSSK();

            this.contactfile.put("commssk.privkey", ssk.privkey);
            this.contactfile.put("commssk.pubkey", ssk.pubkey);
            // we've just generated a new SSK, so the other party definitely doesn't know about it
            this.contactfile.put("status", "notsent");
        }

        return ssk;
    }

    private SSKKeyPair getAckKeyPair() throws ConnectionTerminatedException, InterruptedException {
        SSKKeyPair ssk = new SSKKeyPair();

        ssk.pubkey = this.contactfile.get("ackssk.pubkey");
        ssk.privkey = this.contactfile.get("ackssk.privkey");

        if (ssk.pubkey == null || ssk.privkey == null) {
            HighLevelFCPClient cli = new HighLevelFCPClient();
            ssk = cli.makeSSK();

            this.contactfile.put("ackssk.privkey", ssk.privkey);
            this.contactfile.put("ackssk.pubkey", ssk.pubkey);
        }

        return ssk;
    }

    private RSAKeyParameters getPubKey() throws ConnectionTerminatedException, InterruptedException {
        String mod_str = this.contactfile.get("asymkey.modulus");
        String exp_str = this.contactfile.get("asymkey.pubexponent");

        if (mod_str == null || exp_str == null) {
            // we don't have their mailsite - fetch it
            if (this.fetchMailSite()) {
                mod_str = this.contactfile.get("asymkey.modulus");
                exp_str = this.contactfile.get("asymkey.pubexponent");

                // must be present now, or exception would have been thrown
            } else {
                return null;
            }
        }

        return new RSAKeyParameters(false, new BigInteger(mod_str, 32), new BigInteger(exp_str, 32));
    }

    private String getRtsKsk() throws ConnectionTerminatedException, InterruptedException {
        String rtsksk = this.contactfile.get("rtsksk");

        if (rtsksk == null) {
            // get it from their mailsite
            if (!this.fetchMailSite())
                return null;

            rtsksk = this.contactfile.get("rtsksk");
        }

        return rtsksk;
    }

    /**
     * Get the first slot from which all messages that are still 'in transit' can be retrieved.
     * That is to say, if we have message IDs 3,4 and 5 in transit, this would return the slot
     * for message 3. If there are no messages in transit, returns the next slot on which a message will be inserted.
     */
    private String getCurrentLowestSlot() {
        Set<QueuedMessage> queue = getSendQueue(null);
        int lowestUid = Integer.MAX_VALUE;
        String lowestSlot = null;

        for (QueuedMessage msg : queue) {
            if (msg.uid < lowestUid) {
                lowestUid = msg.uid;
                lowestSlot = msg.slot;
            }
        }
        if (lowestUid < Integer.MAX_VALUE)
            return lowestSlot;

        // No messages in the queue, so the current lowest slot is the
        // next slot we'll insert a message to.
        String retval = this.contactfile.get("nextslot");
        if (retval != null) {
            return retval;
        } else {
            return generateFirstSlot();
        }
    }

    private String generateFirstSlot() {
        Logger.minor(this, "Generating first slot for contact");
        SecureRandom rnd = new SecureRandom();
        SHA256Digest sha256 = new SHA256Digest();
        byte[] buf = new byte[sha256.getDigestSize()];

        rnd.nextBytes(buf);

        String firstSlot = Base32.encode(buf);

        this.contactfile.put("nextslot", Base32.encode(buf));

        return firstSlot;
    }

    private byte[] getAESParams() {
        String params = this.contactfile.get("aesparams");
        if (params != null) {
            return Base64.decode(params);
        }

        SecureRandom rnd = new SecureRandom();
        byte[] retval = new byte[AES_KEY_LENGTH + AES_BLOCK_LENGTH];
        rnd.nextBytes(retval);

        // save them for next time (if insertion fails) so we can
        // generate the same message, otherwise they'll collide
        // unnecessarily.
        this.contactfile.put("aesparams", new String(Base64.encode(retval)));

        return retval;
    }

    /**
     * Set up an outbound contact. Fetch the mailsite, generate a new SSK keypair and post an RTS message to the appropriate KSK.
     * Will block for mailsite retrieval and RTS insertion
     *
     * @return true for success
     */
    private boolean init() throws ConnectionTerminatedException, InterruptedException {
        Logger.normal(this, "Initialising Outbound Contact " + address.toString());

        // try to fetch get all necessary info. will fetch mailsite / generate new keys if necessary
        String initialslot = this.getCurrentLowestSlot();
        SSKKeyPair commssk = this.getCommKeyPair();
        if (commssk == null)
            return false;
        SSKKeyPair ackssk = this.getAckKeyPair();
        RSAKeyParameters their_pub_key = this.getPubKey();
        if (their_pub_key == null)
            return false;
        String rtsksk = this.getRtsKsk();
        if (rtsksk == null)
            return false;

        StringBuffer rtsmessage = new StringBuffer();

        // the public part of the SSK keypair we generated
        rtsmessage.append("commssk=" + commssk.pubkey + "\r\n");

        rtsmessage.append("ackssk=" + ackssk.privkey + "\r\n");

        rtsmessage.append("initialslot=" + initialslot + "\r\n");

        rtsmessage.append("messagetype=rts\r\n");

        // must include who this RTS is to, otherwise we're vulnerable to surreptitious forwarding
        rtsmessage.append("to=" + this.address.getSubDomain() + "\r\n");

        // get our mailsite URI
        String our_mailsite_uri = account.getProps().get("mailsite.pubkey");

        rtsmessage.append("mailsite=" + our_mailsite_uri + "\r\n");

        rtsmessage.append("\r\n");
        //FreemailLogger.normal(this,rtsmessage.toString());

        // sign the message
        SHA256Digest sha256 = new SHA256Digest();
        sha256.update(rtsmessage.toString().getBytes(), 0, rtsmessage.toString().getBytes().length);
        byte[] hash = new byte[sha256.getDigestSize()];
        sha256.doFinal(hash, 0);

        RSAKeyParameters our_priv_key = AccountManager.getPrivateKey(account.getProps());

        AsymmetricBlockCipher sigcipher = new RSAEngine();
        sigcipher.init(true, our_priv_key);
        byte[] sig = null;
        try {
            sig = sigcipher.processBlock(hash, 0, hash.length);
        } catch (InvalidCipherTextException icte) {
            Logger.error(this, "Failed to RSA encrypt hash: " + icte.getMessage());
            icte.printStackTrace();
            return false;
        }

        ByteArrayOutputStream bos = new ByteArrayOutputStream();

        try {
            bos.write(rtsmessage.toString().getBytes());
            bos.write(sig);
        } catch (IOException ioe) {
            ioe.printStackTrace();
            return false;
        }

        // make up a symmetric key
        PaddedBufferedBlockCipher aescipher = new PaddedBufferedBlockCipher(new CBCBlockCipher(new AESEngine()),
                new PKCS7Padding());

        // quick paranoia check!
        if (aescipher.getBlockSize() != AES_BLOCK_LENGTH) {
            // bouncycastle must have changed their implementation, so 
            // we're in trouble
            Logger.normal(this,
                    "Incompatible block size change detected in cryptography API! Are you using a newer version of the bouncycastle libraries? If so, we suggest you downgrade for now, or check for a newer version of Freemail.");
            return false;
        }

        byte[] aes_iv_and_key = this.getAESParams();

        // now encrypt that with our recipient's public key
        AsymmetricBlockCipher enccipher = new RSAEngine();
        enccipher.init(true, their_pub_key);
        byte[] encrypted_aes_params = null;
        try {
            encrypted_aes_params = enccipher.processBlock(aes_iv_and_key, 0, aes_iv_and_key.length);
        } catch (InvalidCipherTextException icte) {
            Logger.error(this,
                    "Failed to perform asymmertic encryption on RTS symmetric key: " + icte.getMessage());
            icte.printStackTrace();
            return false;
        }

        // now encrypt the message with the symmetric key
        KeyParameter kp = new KeyParameter(aes_iv_and_key, aescipher.getBlockSize(), AES_KEY_LENGTH);
        ParametersWithIV kpiv = new ParametersWithIV(kp, aes_iv_and_key, 0, aescipher.getBlockSize());
        aescipher.init(true, kpiv);

        byte[] encmsg = new byte[aescipher.getOutputSize(bos.toByteArray().length) + encrypted_aes_params.length];
        System.arraycopy(encrypted_aes_params, 0, encmsg, 0, encrypted_aes_params.length);
        int offset = encrypted_aes_params.length;
        offset += aescipher.processBytes(bos.toByteArray(), 0, bos.toByteArray().length, encmsg, offset);

        try {
            aescipher.doFinal(encmsg, offset);
        } catch (InvalidCipherTextException icte) {
            Logger.error(this, "Failed to perform symmertic encryption on RTS data: " + icte.getMessage());
            icte.printStackTrace();
            return false;
        }

        // insert it!
        HighLevelFCPClient cli = new HighLevelFCPClient();
        if (cli.slotInsert(encmsg, "KSK@" + rtsksk + "-" + DateStringFactory.getKeyString(), 1, "") < 0) {
            // safe to copy the message into the contact outbox though
            return false;
        }

        // remember the fact that we have successfully inserted the rts
        this.contactfile.put("status", "rts-sent");
        // and remember when we sent it!
        this.contactfile.put("rts-sent-at", Long.toString(System.currentTimeMillis()));
        // and since that's been successfully inserted to that key, we can
        // throw away the symmetric key
        this.contactfile.remove("aesparams");

        Logger.normal(this, "Succesfully initialised Outbound Contact");

        return true;
    }

    // fetch the redirect (assumes that this is a KSK address)
    private String fetchKSKRedirect(String key)
            throws OutboundContactFatalException, ConnectionTerminatedException, InterruptedException {
        HighLevelFCPClient cli = new HighLevelFCPClient();

        Logger.normal(this, "Attempting to fetch mailsite redirect " + key);
        File result;
        try {
            result = cli.fetch(key);
        } catch (FCPFetchException fe) {
            Logger.normal(this, "Failed to retrieve mailsite redirect " + key + " (" + fe.getMessage() + ")");
            return null;
        } catch (FCPException e) {
            Logger.error(this, "Unknown error while fetching mailsite redirect: " + e);
            return null;
        }

        if (result.length() > 512) {
            Logger.normal(this, "Fatal: mailsite redirect too long. Ignoring.");
            result.delete();
            throw new OutboundContactFatalException("Mailsite redirect too long.");
        }

        BufferedReader br = null;
        try {
            br = new BufferedReader(new FileReader(result));
        } catch (FileNotFoundException fnfe) {
            // impossible
            throw new AssertionError();
        }

        String addr;
        try {
            addr = br.readLine();
            br.close();
        } catch (IOException ioe) {
            Logger.normal(this, "Warning: IO exception whilst reading mailsite redirect file: " + ioe.getMessage());
            return null;
        }
        result.delete();
        Logger.normal(this, "Mailsite redirect fetched successfully");
        return addr;
    }

    private boolean fetchMailSite() throws ConnectionTerminatedException, InterruptedException {
        String lastFetchedStr = this.contactfile.get("lastfetched");
        long lastFetched = 0;
        if (lastFetchedStr != null) {
            lastFetched = Long.parseLong(lastFetchedStr);
        }
        if (lastFetched > System.currentTimeMillis()) {
            Logger.error(this,
                    "Mailsite was apparently last fetched in the future! System time gone backwards? Refetching.");
            lastFetched = 0;
        }
        if (lastFetched > System.currentTimeMillis() - MAILSITE_CACHE_TIME) {
            return true;
        }

        HighLevelFCPClient cli = new HighLevelFCPClient();

        Logger.normal(this, "Attempting to fetch " + this.address.getMailpageKey());
        File mailsite_file;
        try {
            mailsite_file = cli.fetch(this.address.getMailpageKey());
        } catch (FCPFetchException fe) {
            Logger.normal(this, "Failed to retrieve mailsite " + this.address.getMailpageKey());
            return false;
        } catch (FCPException e) {
            Logger.error(this, "Unknown error while fetching mailsite: " + e);
            return false;
        }

        Logger.normal(this, "got mailsite");

        PropsFile mailsite = PropsFile.createPropsFile(mailsite_file);

        String rtsksk = mailsite.get("rtsksk");
        String keymod_str = mailsite.get("asymkey.modulus");
        String keyexp_str = mailsite.get("asymkey.pubexponent");

        mailsite_file.delete();

        if (rtsksk == null || keymod_str == null || keyexp_str == null) {
            // Not actually fatal - the other party could publish a new, valid mailsite
            Logger.normal(this, "Mailsite for " + this.address + " does not contain all necessary information!");
            return false;
        }

        // add this to a new outbound contact file
        this.contactfile.put("rtsksk", rtsksk);
        this.contactfile.put("asymkey.modulus", keymod_str);
        this.contactfile.put("asymkey.pubexponent", keyexp_str);
        this.contactfile.put("lastfetched", Long.toString(System.currentTimeMillis()));

        return true;
    }

    private String popNextSlot() {
        String slot = this.contactfile.get("nextslot");
        if (slot == null) {
            return generateFirstSlot();
        }
        SHA256Digest sha256 = new SHA256Digest();
        sha256.update(Base32.decode(slot), 0, Base32.decode(slot).length);
        byte[] nextslot = new byte[sha256.getDigestSize()];
        sha256.doFinal(nextslot, 0);
        this.contactfile.put("nextslot", Base32.encode(nextslot));

        return slot;
    }

    private int popNextUid() {
        String nextuid_s = this.contactfile.get("nextuid");

        int nextuid;
        if (nextuid_s == null)
            nextuid = 1;
        else
            nextuid = Integer.parseInt(nextuid_s);

        this.contactfile.put("nextuid", Integer.toString(nextuid + 1));
        return nextuid;
    }

    public boolean sendMessage(File body) {
        int uid = this.popNextUid();

        // create a new file that contains the complete Freemail
        // message, with Freemail headers
        QueuedMessage qm = new QueuedMessage(uid);

        File msg;
        PrintWriter pw;
        try {
            msg = File.createTempFile("ogm", "msg", Freemail.getTempDir());

            pw = new PrintWriter(new FileOutputStream(msg));
        } catch (IOException ioe) {
            Logger.normal(this, "IO Error encountered whilst trying to send message: " + ioe.getMessage()
                    + " Will try again soon");
            return false;
        }

        try {
            pw.print("id=" + uid + "\r\n\r\n");

            BufferedReader br = new BufferedReader(new FileReader(body));
            MailHeaderFilter filter = new MailHeaderFilter(br);

            String chunk;
            while ((chunk = filter.readHeader()) != null) {
                pw.print(chunk + "\r\n");
            }
            pw.print("\r\n");

            // Headers are done, copy the rest
            while ((chunk = br.readLine()) != null) {
                pw.print(chunk + "\r\n");
            }

            pw.close();
            br.close();
        } catch (IOException ioe) {
            Logger.normal(this, "IO Error encountered whilst trying to send message: " + ioe.getMessage()
                    + " Will try again soon");
            qm.delete();
            msg.delete();
            return false;
        }

        String slot = this.popNextSlot();

        qm.slot = slot;

        if (qm.setMessageFile(msg) && qm.saveProps()) {
            return true;
        }
        return false;
    }

    public void doComm(long timeout) throws InterruptedException {
        try {
            this.sendQueued(timeout / 2);
            this.pollAcks(timeout / 2);
            this.checkCTS();
        } catch (ConnectionTerminatedException cte) {
            // just exit
        }
    }

    private void sendQueued(long timeout) throws ConnectionTerminatedException, InterruptedException {
        boolean ready;
        String ctstatus = this.contactfile.get("status");
        if (ctstatus == null)
            ctstatus = "notsent";
        if (ctstatus.equals("rts-sent") || ctstatus.equals("cts-received")) {
            ready = true;
        } else {
            ready = this.init();
        }

        HighLevelFCPClient fcpcli = null;

        /* We sort the messages by uid since this is the order the other side will
         * attempt to fetch the messages. Using the same order improves performance
         * when sending a lot of messages. */
        Set<QueuedMessage> msgs = this.getSendQueue(new MessageUidComparator());

        long start = System.nanoTime();
        for (QueuedMessage msg : msgs) {
            if (msg.last_send_time > 0)
                continue;

            if (!ready) {
                if (msg.added_time + FAIL_DELAY < System.currentTimeMillis()) {
                    if (Postman.bounceMessage(msg.getMessageFile(), account.getMessageBank(),
                            "Freemail has been trying to establish a communication channel with this party for too long "
                                    + "without success. Check that the Freemail address is valid, and that the recipient still runs "
                                    + "Freemail on at least a semi-regular basis.",
                            true)) {
                        msg.delete();
                    }
                }
                continue;
            }

            if (fcpcli == null)
                fcpcli = new HighLevelFCPClient();

            String key = this.contactfile.get("commssk.privkey");

            if (key == null) {
                Logger.normal(this,
                        "Contact file does not contain private communication key! It appears that your Freemail directory is corrupt!");
                continue;
            }

            if (msg.slot == null) {
                Logger.normal(this,
                        "Index file does not contain slot name for this message, the mail cannot be sent this way.");
                Logger.debug(this, "Filename is " + contactfile.toString());
                continue;
            }

            key += msg.slot;

            FileInputStream fis;
            try {
                fis = new FileInputStream(msg.file);
            } catch (FileNotFoundException fnfe) {
                continue;
            }

            Logger.normal(this, "Inserting message");
            Logger.debug(this, "Insert key is " + key);
            FCPPutFailedException err;
            try {
                err = fcpcli.put(fis, key);
            } catch (FCPBadFileException bfe) {
                Logger.normal(this, "Failed sending message. Will try again soon.");
                continue;
            } catch (FCPException e) {
                Logger.error(this, "Unknown error while sending message: " + e);
                continue;
            }
            if (err == null) {
                Logger.normal(this, "Successfully inserted message");
                Logger.debug(this, "Insert key was " + key);
                if (msg.first_send_time < 0)
                    msg.first_send_time = System.currentTimeMillis();
                msg.last_send_time = System.currentTimeMillis();
                msg.saveProps();
            } else if (err.errorcode == FCPPutFailedException.COLLISION) {
                msg.slot = popNextSlot();
                Logger.error(this, "Insert collided! Assigned new slot");
                Logger.debug(this, "New slot is " + msg.slot);
                msg.saveProps();
            } else if (msg.added_time + FAIL_DELAY < System.currentTimeMillis()) {
                Logger.normal(this, "Giving up on a message - been trying to send for too long. Bouncing.");
                if (Postman.bounceMessage(msg.getMessageFile(), account.getMessageBank(),
                        "Freemail has been trying to deliver this message for too long without success. "
                                + "This is likley to be due to a poor connection to Freenet. Check your Freenet node.",
                        true)) {
                    msg.delete();
                }
            } else {
                Logger.normal(this,
                        "Failed to insert message (error code " + err.errorcode + ") will try again soon.");
                if (err.errorcode == FCPPutFailedException.COLLISION) {
                    Logger.error(this,
                            "Failed to insert message, will try again soon. (Collision, this shouldn't happen)");
                } else {
                    Logger.normal(this, "Failed to insert message, will try again soon. Error: " + err.errorcode);
                }
                Logger.debug(this, "Insert key was " + key);
            }

            if (System.nanoTime() > (start + (timeout * 1000 * 1000))) {
                Logger.debug(this, "Stopping message sending due to timeout");
                break;
            }
        }
    }

    private void pollAcks(long timeout) throws ConnectionTerminatedException, InterruptedException {
        HighLevelFCPClient fcpcli = null;
        Set<QueuedMessage> msgs = this.getSendQueue(null);

        Logger.debug(this, "Starting from ack index " + nextAckIndex);
        long start = System.nanoTime();
        int ackIndex = 0;
        for (QueuedMessage msg : msgs) {
            if (ackIndex++ < nextAckIndex)
                continue;
            if (msg.first_send_time < 0)
                continue;

            if (fcpcli == null)
                fcpcli = new HighLevelFCPClient();

            String key = this.contactfile.get("ackssk.pubkey");
            if (key == null) {
                Logger.normal(this,
                        "Contact file does not contain public ack key! It appears that your Freemail directory is corrupt!");
                continue;
            }

            key += "ack-" + msg.uid;

            Logger.minor(this, "Looking for message ack");
            Logger.debug(this, "Ack key is " + key);

            try {
                File ack = fcpcli.fetch(key);
                Logger.normal(this, "Ack received for message " + msg.uid + " on contact " + this.address.domain
                        + ". Now that's a job well done.");
                ack.delete();
                msg.delete();
                // treat the ACK as a CTS too
                this.contactfile.put("status", "cts-received");
                // delete initial slot for forward secrecy
                this.contactfile.remove("initialslot");
            } catch (FCPFetchException fe) {
                Logger.minor(this, "Failed to receive ack (" + fe.getMessage() + ")");
                Logger.debug(this, "Ack key was " + key);
                if (!fe.isNetworkError()) {
                    if (System.currentTimeMillis() > msg.first_send_time + FAIL_DELAY) {
                        // give up and bounce the message
                        File m = msg.getMessageFile();

                        Postman.bounceMessage(m, account.getMessageBank(),
                                "Freemail has been trying for too long to deliver this message, and has received no acknowledgement. "
                                        + "It is possible that the recipient has not run Freemail since you sent the message. "
                                        + "If you believe this is likely, try resending the message.",
                                true);
                        Logger.normal(this, "Giving up on message - been trying for too long.");
                        msg.delete();
                    } else if (System.currentTimeMillis() > msg.last_send_time + RTS_RETRANSMIT_DELAY) {
                        Logger.normal(this, "Resending RTS for contact");
                        init();

                        // bit of a fudge - this won't actually be the last send time, since we won't
                        // re-send messages at all now, it will be the last time the RTS was sent.
                        // Hack: We update the time for all the messages that have been sent since
                        // we only want to resend the RTS once, not once per message
                        for (QueuedMessage message : msgs) {
                            message.last_send_time = System.currentTimeMillis();
                            message.saveProps();
                        }
                    }
                }
            } catch (FCPException e) {
                Logger.error(this, "Unknown error while fetching ack: " + e);
                Logger.debug(this, "Key was key " + key);
                //Don't check the timeout here so we get at least one proper fetch attempt if this
                //is a temporary problem
            }

            if (System.nanoTime() > start + (timeout * 1000 * 1000)) {
                Logger.debug(this, "Stopping ack fetching due to timeout");
                break;
            }
        }

        nextAckIndex = ackIndex;
        if (nextAckIndex >= msgs.size()) {
            nextAckIndex = 0;
        }
    }

    /**
     * Returns the send queue for this contact.
     * @param comparator the Comparator used to sort the queue. If null, the queue will be unsorted.
     * @return the send queue for this contact
     */
    private Set<QueuedMessage> getSendQueue(Comparator<? super QueuedMessage> comparator) {
        File[] files = ctoutbox.listFiles();
        Set<QueuedMessage> msgs;
        if (comparator == null) {
            msgs = new HashSet<QueuedMessage>();
        } else {
            msgs = new TreeSet<QueuedMessage>(comparator);
        }

        int i;
        for (i = 0; i < files.length; i++) {
            if (files[i].getName().equals(QueuedMessage.INDEX_FILE))
                continue;

            int uid;
            try {
                uid = Integer.parseInt(files[i].getName());
            } catch (NumberFormatException nfe) {
                // how did that get there? just delete it
                Logger.normal(this, "Found spurious file in send queue: '" + files[i].getName() + "' - deleting.");
                files[i].delete();
                continue;
            }

            msgs.add(new QueuedMessage(uid));
        }

        return msgs;
    }

    private class QueuedMessage {
        static final String INDEX_FILE = "_index";

        PropsFile index;

        final int uid;
        String slot;
        long added_time;
        long first_send_time;
        long last_send_time;
        private final File file;

        public QueuedMessage(int uid) {
            this.uid = uid;
            this.file = new File(ctoutbox, Integer.toString(uid));

            this.index = PropsFile.createPropsFile(new File(ctoutbox, INDEX_FILE));

            this.slot = this.index.get(uid + ".slot");
            String s_first = this.index.get(uid + ".first_send_time");
            if (s_first == null)
                this.first_send_time = -1;
            else
                this.first_send_time = Long.parseLong(s_first);

            String s_last = this.index.get(uid + ".last_send_time");
            if (s_last == null)
                this.last_send_time = -1;
            else
                this.last_send_time = Long.parseLong(s_last);

            String s_added = this.index.get(uid + ".added_time");
            if (s_added == null)
                this.added_time = System.currentTimeMillis();
            else
                this.added_time = Long.parseLong(s_added);
        }

        public FileInputStream getInputStream() throws FileNotFoundException {
            return new FileInputStream(this.file);
        }

        public File getMessageFile() {
            return this.file;
        }

        public boolean setMessageFile(File newfile) {
            return newfile.renameTo(this.file);
        }

        public boolean saveProps() {
            boolean suc = true;
            suc &= this.index.put(uid + ".slot", this.slot);
            suc &= this.index.put(uid + ".first_send_time", this.first_send_time);
            suc &= this.index.put(uid + ".last_send_time", this.last_send_time);
            suc &= this.index.put(uid + ".added_time", this.added_time);

            return suc;
        }

        public boolean delete() {
            this.index.remove(this.uid + ".slot");
            this.index.remove(this.uid + ".first_send_time");
            this.index.remove(this.uid + ".last_send_time");

            return this.file.delete();
        }
    }

    private class MessageUidComparator implements Comparator<QueuedMessage> {
        @Override
        public int compare(QueuedMessage msg1, QueuedMessage msg2) {
            return msg1.uid - msg2.uid;
        }
    }
}