i2p.bote.I2PBote.java Source code

Java tutorial

Introduction

Here is the source code for i2p.bote.I2PBote.java

Source

/**
 * Copyright (C) 2009  HungryHobo@mail.i2p
 * 
 * The GPG fingerprint for HungryHobo@mail.i2p is:
 * 6DD3 EAA2 9990 29BC 4AD2 7486 1E2C 7B61 76DC DC12
 * 
 * This file is part of I2P-Bote.
 * I2P-Bote 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 3 of the License, or
 * (at your option) any later version.
 * 
 * I2P-Bote 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 I2P-Bote.  If not, see <http://www.gnu.org/licenses/>.
 */

package i2p.bote;

import static i2p.bote.Util._t;
import i2p.bote.addressbook.AddressBook;
import i2p.bote.crypto.wordlist.WordListAnchor;
import i2p.bote.debug.DebugSupport;
import i2p.bote.email.Email;
import i2p.bote.email.EmailIdentity;
import i2p.bote.email.Identities;
import i2p.bote.fileencryption.DerivedKey;
import i2p.bote.fileencryption.FileEncryptionUtil;
import i2p.bote.fileencryption.PasswordCache;
import i2p.bote.fileencryption.PasswordCacheListener;
import i2p.bote.fileencryption.PasswordException;
import i2p.bote.fileencryption.PasswordVerifier;
import i2p.bote.folder.DirectoryEntryFolder;
import i2p.bote.folder.EmailFolder;
import i2p.bote.folder.EmailFolderManager;
import i2p.bote.folder.EmailPacketFolder;
import i2p.bote.folder.IncompleteEmailFolder;
import i2p.bote.folder.IndexPacketFolder;
import i2p.bote.folder.MessageIdCache;
import i2p.bote.folder.NewEmailListener;
import i2p.bote.folder.Outbox;
import i2p.bote.folder.RelayPacketFolder;
import i2p.bote.folder.TrashFolder;
import i2p.bote.imap.ImapService;
import i2p.bote.migration.Migrator;
import i2p.bote.network.BanList;
import i2p.bote.network.BannedPeer;
import i2p.bote.network.DhtException;
import i2p.bote.network.DhtPeerStats;
import i2p.bote.network.DhtResults;
import i2p.bote.network.I2PPacketDispatcher;
import i2p.bote.network.I2PSendQueue;
import i2p.bote.network.NetworkStatus;
import i2p.bote.network.NetworkStatusListener;
import i2p.bote.network.NetworkStatusSource;
import i2p.bote.network.RelayPacketHandler;
import i2p.bote.network.RelayPeer;
import i2p.bote.network.kademlia.KademliaDHT;
import i2p.bote.packet.dht.Contact;
import i2p.bote.packet.dht.DhtStorablePacket;
import i2p.bote.packet.dht.EncryptedEmailPacket;
import i2p.bote.packet.dht.IndexPacket;
import i2p.bote.service.DeliveryChecker;
import i2p.bote.service.EmailChecker;
import i2p.bote.service.ExpirationThread;
import i2p.bote.service.OutboxListener;
import i2p.bote.service.OutboxProcessor;
import i2p.bote.service.RelayPacketSender;
import i2p.bote.service.RelayPeerManager;
import i2p.bote.service.seedless.SeedlessInitializer;
import i2p.bote.smtp.SmtpService;

import java.io.BufferedWriter;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.lang.Thread.State;
import java.net.URISyntaxException;
import java.net.UnknownHostException;
import java.security.GeneralSecurityException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

import javax.mail.MessagingException;

import net.i2p.I2PAppContext;
import net.i2p.I2PException;
import net.i2p.client.I2PClient;
import net.i2p.client.I2PClientFactory;
import net.i2p.client.I2PSession;
import net.i2p.client.I2PSessionException;
import net.i2p.client.streaming.I2PSocketManager;
import net.i2p.client.streaming.I2PSocketManagerFactory;
import net.i2p.data.Base64;
import net.i2p.data.DataFormatException;
import net.i2p.data.Destination;
import net.i2p.data.Hash;
import net.i2p.util.I2PAppThread;
import net.i2p.util.Log;
import net.i2p.util.SecureFile;
import net.i2p.util.SecureFileOutputStream;

import org.apache.commons.configuration.ConfigurationException;

/**
 * This is the core class of the application. It is implemented as a singleton.
 */
public class I2PBote implements NetworkStatusSource, EmailFolderManager, MailSender, PasswordVerifier {
    public static final int PROTOCOL_VERSION = 4;
    private static final String APP_VERSION = "0.4.2";
    private static final int STARTUP_DELAY = 3; // the number of minutes to wait before connecting to I2P (this gives the router time to get ready)
    private static volatile I2PBote instance;

    private Log log = new Log(I2PBote.class);
    private I2PClient i2pClient;
    private I2PSession i2pSession;
    private I2PSocketManager socketManager;
    private Configuration configuration;
    private Identities identities;
    private AddressBook addressBook;
    private Outbox outbox; // stores outgoing emails for all local users
    private EmailFolder inbox; // stores incoming emails for all local users
    private EmailFolder sentFolder;
    private TrashFolder trashFolder;
    private RelayPacketFolder relayPacketFolder; // stores email packets we're forwarding for other machines
    private IncompleteEmailFolder incompleteEmailFolder; // stores email packets addressed to a local user
    private EmailPacketFolder emailDhtStorageFolder; // stores email packets for other peers
    private IndexPacketFolder indexPacketDhtStorageFolder; // stores index packets
    private DirectoryEntryFolder directoryDhtFolder; // stores entries for the distributed address directory
    private WordListAnchor wordLists;
    private Collection<I2PAppThread> backgroundThreads;
    private SmtpService smtpService;
    private ImapService imapService;
    private OutboxProcessor outboxProcessor; // reads emails stored in the outbox and sends them
    private EmailChecker emailChecker;
    private DeliveryChecker deliveryChecker;
    private KademliaDHT dht;
    private RelayPeerManager peerManager;
    private PasswordCache passwordCache;
    private Future<Void> passwordChangeResult;
    private ConnectTask connectTask;
    private DebugSupport debugSupport;
    private Collection<NetworkStatusListener> networkStatusListeners;

    /**
     * Constructs a new instance of <code>I2PBote</code> and initializes
     * folders and a few other things. No background threads are spawned,
     * and network connectitivy is not initialized.
     */
    private I2PBote() {
        Thread.currentThread().setName("I2PBoteMain");

        I2PAppContext appContext = I2PAppContext.getGlobalContext();
        appContext.addShutdownTask(new Runnable() {
            @Override
            public void run() {
                shutDown();
            }
        });
        i2pClient = I2PClientFactory.createClient();
        configuration = new Configuration();

        final Migrator migrator = new Migrator(configuration, APP_VERSION);
        migrator.migrateNonPasswordedDataIfNeeded();

        passwordCache = new PasswordCache(configuration);
        // purge identities and addresses from memory when the password is cleared
        passwordCache.addPasswordCacheListener(new PasswordCacheListener() {
            @Override
            public void passwordProvided() {
                migrator.migratePasswordedDataIfNeeded(passwordCache);
            }

            @Override
            public void passwordCleared() {
                identities.clearPasswordProtectedData();
                addressBook.clearPasswordProtectedData();
            }
        });
        identities = new Identities(configuration.getIdentitiesFile(), passwordCache);
        addressBook = new AddressBook(configuration.getAddressBookFile(), passwordCache);
        initializeFolderAccess(passwordCache);
        initializeExternalThemeDir();

        debugSupport = new DebugSupport(configuration, passwordCache);

        wordLists = new WordListAnchor();

        networkStatusListeners = new ArrayList<NetworkStatusListener>();
    }

    /**
     * Initializes objects for accessing emails and packet files on the filesystem.
     * @param passwordCache
     */
    private void initializeFolderAccess(PasswordCache passwordCache) {
        inbox = new EmailFolder(configuration.getInboxDir(), passwordCache);
        outbox = new Outbox(configuration.getOutboxDir(), passwordCache);
        sentFolder = new EmailFolder(configuration.getSentFolderDir(), passwordCache);
        trashFolder = new TrashFolder(configuration.getTrashFolderDir(), passwordCache);
        relayPacketFolder = new RelayPacketFolder(configuration.getRelayPacketDir());
        MessageIdCache messageIdCache = new MessageIdCache(configuration.getMessageIdCacheFile(),
                configuration.getMessageIdCacheSize());
        incompleteEmailFolder = new IncompleteEmailFolder(configuration.getIncompleteDir(), messageIdCache, inbox);
        emailDhtStorageFolder = new EmailPacketFolder(configuration.getEmailDhtStorageDir());
        indexPacketDhtStorageFolder = new IndexPacketFolder(configuration.getIndexPacketDhtStorageDir());
        directoryDhtFolder = new DirectoryEntryFolder(configuration.getDirectoryEntryDhtStorageDir());
    }

    /** Creates the external themes directory if it doesn't exist */
    private void initializeExternalThemeDir() {
        File dir = configuration.getExternalThemeDir();
        if (!dir.exists() && !dir.mkdirs())
            log.error("Can't create directory: <" + dir.getAbsolutePath() + ">");
    }

    /**
     * Sets up a {@link I2PSession}, using the I2P destination stored on disk or creating a new I2P
     * destination if no key file exists.
     */
    private void initializeSession() throws I2PSessionException {
        Properties sessionProperties = new Properties();
        // set tunnel names
        sessionProperties.setProperty("inbound.nickname", "I2P-Bote");
        sessionProperties.setProperty("outbound.nickname", "I2P-Bote");
        if (configuration.isI2CPDomainSocketEnabled())
            sessionProperties.setProperty("i2cp.domainSocket", "true");
        // According to sponge, muxed depends on gzip, so leave gzip enabled

        // read the local destination key from the key file if it exists
        File destinationKeyFile = configuration.getDestinationKeyFile();
        FileReader fileReader = null;
        try {
            fileReader = new FileReader(destinationKeyFile);
            char[] destKeyBuffer = new char[(int) destinationKeyFile.length()];
            fileReader.read(destKeyBuffer);
            byte[] localDestinationKey = Base64.decode(new String(destKeyBuffer));
            ByteArrayInputStream inputStream = new ByteArrayInputStream(localDestinationKey);
            socketManager = I2PSocketManagerFactory.createDisconnectedManager(inputStream, null, 0,
                    sessionProperties);
        } catch (IOException e) {
            log.debug("Destination key file doesn't exist or isn't readable." + e);
        } catch (I2PSessionException e) {
            // Won't happen, inputStream != null
        } finally {
            if (fileReader != null)
                try {
                    fileReader.close();
                } catch (IOException e) {
                    log.debug("Error closing file: <" + destinationKeyFile.getAbsolutePath() + ">" + e);
                }
        }

        // if the local destination key can't be read or is invalid, create a new one
        if (socketManager == null) {
            log.debug("Creating new local destination key");
            try {
                ByteArrayOutputStream arrayStream = new ByteArrayOutputStream();
                i2pClient.createDestination(arrayStream);
                byte[] localDestinationKey = arrayStream.toByteArray();

                ByteArrayInputStream inputStream = new ByteArrayInputStream(localDestinationKey);
                socketManager = I2PSocketManagerFactory.createDisconnectedManager(inputStream, null, 0,
                        sessionProperties);

                saveLocalDestinationKeys(destinationKeyFile, localDestinationKey);
            } catch (I2PException e) {
                log.error("Error creating local destination key.", e);
            } catch (IOException e) {
                log.error("Error writing local destination key to file.", e);
            }
        }

        i2pSession = socketManager.getSession();
        // Throws I2PSessionException if the connection fails
        i2pSession.connect();

        Destination localDestination = i2pSession.getMyDestination();
        log.info("Local destination key (base64): " + localDestination.toBase64());
        log.info("Local destination hash (base64): " + localDestination.calculateHash().toBase64());
        log.info("Local destination hash (base32): " + Util.toBase32(localDestination));
    }

    /**
     * Initializes daemon threads, doesn't start them yet.
     */
    private void initializeServices() {
        I2PPacketDispatcher dispatcher = new I2PPacketDispatcher();
        i2pSession.addMuxedSessionListener(dispatcher, I2PSession.PROTO_DATAGRAM, I2PSession.PORT_ANY);

        backgroundThreads.add(passwordCache);
        I2PSendQueue sendQueue = new I2PSendQueue(i2pSession, dispatcher);
        backgroundThreads.add(sendQueue);
        RelayPacketSender relayPacketSender = new RelayPacketSender(sendQueue, relayPacketFolder, configuration); // reads packets stored in the relayPacketFolder and sends them
        backgroundThreads.add(relayPacketSender);

        SeedlessInitializer seedless = new SeedlessInitializer(socketManager);
        backgroundThreads.add(seedless);

        dht = new KademliaDHT(sendQueue, dispatcher, configuration.getDhtPeerFile(), seedless);
        backgroundThreads.add(dht);

        dht.setStorageHandler(EncryptedEmailPacket.class, emailDhtStorageFolder);
        dht.setStorageHandler(IndexPacket.class, indexPacketDhtStorageFolder);
        dht.setStorageHandler(Contact.class, directoryDhtFolder);

        peerManager = new RelayPeerManager(sendQueue, getLocalDestination(), configuration.getRelayPeerFile());
        backgroundThreads.add(peerManager);

        dispatcher.addPacketListener(emailDhtStorageFolder);
        dispatcher.addPacketListener(indexPacketDhtStorageFolder);
        dispatcher.addPacketListener(new RelayPacketHandler(relayPacketFolder, dht, sendQueue, i2pSession));
        dispatcher.addPacketListener(peerManager);
        dispatcher.addPacketListener(relayPacketSender);

        ExpirationThread expirationThread = new ExpirationThread();
        expirationThread.addExpirationListener(emailDhtStorageFolder);
        expirationThread.addExpirationListener(indexPacketDhtStorageFolder);
        expirationThread.addExpirationListener(relayPacketSender);
        backgroundThreads.add(expirationThread);

        outboxProcessor = new OutboxProcessor(dht, outbox, peerManager, relayPacketFolder, identities,
                configuration, this);
        outboxProcessor.addOutboxListener(new OutboxListener() {
            /** Moves sent emails to the "sent" folder */
            @Override
            public void emailSent(Email email) {
                try {
                    outbox.setNew(email, false);
                    log.debug("Moving email [" + email + "] to the \"sent\" folder.");
                    outbox.move(email, sentFolder);
                } catch (Exception e) {
                    log.error("Cannot move email from outbox to sent folder: " + email, e);
                }
            }
        });
        backgroundThreads.add(outboxProcessor);

        emailChecker = new EmailChecker(identities, configuration, incompleteEmailFolder, emailDhtStorageFolder,
                indexPacketDhtStorageFolder, this, sendQueue, dht, peerManager);
        backgroundThreads.add(emailChecker);

        deliveryChecker = new DeliveryChecker(dht, sentFolder, configuration, this);
        backgroundThreads.add(deliveryChecker);
    }

    /**
     * Writes private + public keys for the local destination out to a file.
     * @param keyFile
     * @param localDestinationArray
     * @throws DataFormatException
     * @throws IOException
     */
    private void saveLocalDestinationKeys(File keyFile, byte[] localDestinationArray)
            throws DataFormatException, IOException {
        keyFile = new SecureFile(keyFile.getAbsolutePath());
        if (keyFile.exists()) {
            File oldKeyFile = new File(keyFile.getPath() + "_backup");
            if (!keyFile.renameTo(oldKeyFile))
                log.error("Cannot rename destination key file <" + keyFile.getAbsolutePath() + "> to <"
                        + oldKeyFile.getAbsolutePath() + ">");
        } else if (!keyFile.createNewFile())
            log.error("Cannot create destination key file: <" + keyFile.getAbsolutePath() + ">");

        BufferedWriter fileWriter = new BufferedWriter(new OutputStreamWriter(new SecureFileOutputStream(keyFile)));
        try {
            fileWriter.write(Base64.encode(localDestinationArray));
        } finally {
            fileWriter.close();
        }
    }

    /**
     * Initializes network connectivity and starts background threads.<br/>
     * This is done in a separate thread so the webapp thread is not blocked
     * by this method.
     */
    public void startUp() {
        backgroundThreads = new ArrayList<I2PAppThread>();
        connectTask = new ConnectTask();
        backgroundThreads.add(connectTask);
        connectTask.start();

        // TODO Fix log4j loading so IMAP can start
        if (false && configuration.isImapEnabled())
            startImap();
        if (configuration.isSmtpEnabled())
            startSmtp();
    }

    public void shutDown() {
        stopAllServices();

        try {
            if (i2pSession != null)
                i2pSession.destroySession();
        } catch (I2PSessionException e) {
            log.error("Can't destroy I2P session.", e);
        }
        if (socketManager != null)
            socketManager.destroySocketManager();

        connectTask = null;
        networkStatusChanged();
    }

    public static I2PBote getInstance() {
        if (instance == null)
            instance = new I2PBote();
        return instance;
    }

    public Configuration getConfiguration() {
        return configuration;
    }

    public static String getAppVersion() {
        return APP_VERSION;
    }

    /**
     * Returns the current router console language.
     */
    public static String getLanguage() {
        String language = System.getProperty("routerconsole.lang");
        if (language != null)
            return language;
        else
            return Locale.getDefault().getLanguage();
    }

    public Identities getIdentities() {
        return identities;
    }

    public AddressBook getAddressBook() {
        return addressBook;
    }

    /** Publishes an email destination in the address directory. */
    public void publishDestination(String destination, byte[] picture, String text)
            throws PasswordException, IOException, GeneralSecurityException, DhtException, InterruptedException {
        EmailIdentity identity = identities.get(destination);
        if (identity != null) {
            identity.setPicture(picture);
            identity.setText(text);
            if (identity.getFingerprint() == null)
                identity.generateFingerprint(); // if no fingerprint exists, generate one and save it in the next step
            identities.save();
            Contact entry = new Contact(identity, identities, picture, text, identity.getFingerprint());
            dht.store(entry);
        }
    }

    public Contact lookupInDirectory(String name) throws InterruptedException {
        Hash key = EmailIdentity.calculateHash(name);
        if (null == dht) {
            return null;
        }
        DhtResults results = dht.findOne(key, Contact.class);
        if (!results.isEmpty()) {
            DhtStorablePacket packet = results.getPackets().iterator().next();
            if (packet instanceof Contact) {
                Contact contact = (Contact) packet;
                try {
                    if (contact.verify())
                        return contact;
                } catch (GeneralSecurityException e) {
                    log.error("Can't verify Contact", e);
                }
            }
        }
        return null;
    }

    public String[] getWordList(String localeCode) {
        return wordLists.getWordList(localeCode);
    }

    /** Returns all locale codes for which a word list exists. */
    public List<String> getWordListLocales() throws UnsupportedEncodingException, IOException, URISyntaxException {
        return wordLists.getLocaleCodes();
    }

    public Destination getLocalDestination() {
        if (i2pSession == null)
            return null;
        else
            return i2pSession.getMyDestination();
    }

    public void sendEmail(Email email) throws MessagingException, PasswordException, IOException,
            GeneralSecurityException, DataFormatException {
        email.checkAddresses();

        // sign email unless sender is anonymous
        if (!email.isAnonymous()) {
            String sender = email.getOneFromAddress();
            EmailIdentity senderIdentity = identities.extractIdentity(sender);
            if (senderIdentity == null)
                throw new MessagingException(_t("No identity matches the sender/from field: " + sender));
            email.sign(senderIdentity, identities);
        }

        email.setSignatureFlag(); // set the signature flag so the signature isn't reverified every time the email is loaded
        outbox.add(email);
        if (outboxProcessor != null)
            outboxProcessor.checkForEmail();
    }

    public synchronized void checkForMail() throws PasswordException, IOException, GeneralSecurityException {
        if (emailChecker != null)
            emailChecker.checkForMail();
    }

    public synchronized void checkForMail(String key)
            throws PasswordException, IOException, GeneralSecurityException {
        if (emailChecker != null)
            emailChecker.checkForMail(key);
    }

    /**
     * @see EmailChecker#isCheckingForMail()
     */
    public synchronized boolean isCheckingForMail() {
        if (emailChecker == null)
            return false;
        else
            return emailChecker.isCheckingForMail();
    }

    /**
     * @see EmailChecker#isCheckingForMail(EmailIdentity)
     */
    public synchronized boolean isCheckingForMail(EmailIdentity identity) {
        if (emailChecker == null)
            return false;
        else
            return emailChecker.isCheckingForMail(identity);
    }

    /**
     * @see EmailChecker#getLastMailCheckTime()
     */
    public Date getLastMailCheckTime() {
        if (emailChecker == null)
            return null;
        else {
            long time = emailChecker.getLastMailCheckTime();
            return time == 0 ? null : new Date(time);
        }
    }

    /**
     * @see EmailChecker#newMailReceived()
     */
    public boolean newMailReceived() {
        if (emailChecker == null)
            return false;
        else
            return emailChecker.newMailReceived();
    }

    public void setImapEnabled(boolean enabled) {
        configuration.setImapEnabled(enabled);
        if (imapService == null || !imapService.isStarted()) {
            // TODO Fix log4j loading so IMAP can start
            if (false && enabled)
                startImap();
        } else if (imapService != null && imapService.isStarted() && !enabled)
            stopImap();
    }

    private void startImap() {
        try {
            imapService = new ImapService(configuration, this, this);
            if (!imapService.start())
                log.error("IMAP service failed to start.");
        } catch (ConfigurationException e) {
            log.error("IMAP service failed to start.", e);
        }
    }

    private void stopImap() {
        if (imapService != null && !imapService.stop())
            log.error("IMAP service failed to stop");
    }

    public void setSmtpEnabled(boolean enabled) {
        configuration.setSmtpEnabled(enabled);
        if (smtpService == null || !smtpService.isRunning()) {
            if (enabled)
                startSmtp();
        } else if (smtpService != null && smtpService.isRunning() && !enabled)
            stopSmtp();
    }

    private void startSmtp() {
        try {
            smtpService = new SmtpService(configuration, this, this);
            smtpService.start();
        } catch (UnknownHostException e) {
            log.error("SMTP service failed to start.");
        }
    }

    private void stopSmtp() {
        if (smtpService != null)
            smtpService.stop();
    }

    public EmailFolder getInbox() {
        return inbox;
    }

    public Outbox getOutbox() {
        return outbox;
    }

    public EmailFolder getSentFolder() {
        return sentFolder;
    }

    public EmailFolder getTrashFolder() {
        return trashFolder;
    }

    public int getNumIncompleteEmails() {
        return incompleteEmailFolder.getNumIncompleteEmails();
    }

    public void addNewEmailListener(NewEmailListener newEmailListener) {
        incompleteEmailFolder.addNewEmailListener(newEmailListener);
    }

    public void removeNewEmailListener(NewEmailListener newEmailListener) {
        incompleteEmailFolder.removeNewEmailListener(newEmailListener);
    }

    public boolean deleteEmail(EmailFolder folder, String messageId) {
        if (folder instanceof TrashFolder)
            return folder.delete(messageId);
        else
            return folder.move(messageId, trashFolder);
    }

    /**
     * Calls {@link #changePassword(byte[], byte[], byte[])} in a new thread and
     * returns a {@link Future} that throws the same exceptions the synchronous
     * variant would.
     * @param oldPassword
     * @param newPassword
     * @param confirmNewPassword
     */
    public void changePasswordAsync(final byte[] oldPassword, final byte[] newPassword,
            final byte[] confirmNewPassword) {
        passwordChangeResult = Executors.newSingleThreadExecutor().submit(new Callable<Void>() {
            @Override
            public Void call() throws IOException, GeneralSecurityException, PasswordException {
                changePassword(oldPassword, newPassword, confirmNewPassword);
                return null;
            }
        });
    }

    public void waitForPasswordChange() throws Throwable {
        if (passwordChangeResult == null)
            return;

        try {
            passwordChangeResult.get();
        } catch (ExecutionException e) {
            throw e.getCause();
        } finally {
            passwordChangeResult = null;
        }
    }

    /**
     * Reencrypts all encrypted files with a new password
     * @param oldPassword
     * @param newPassword
     * @param confirmNewPassword
     * @throws IOException 
     * @throws GeneralSecurityException 
     * @throws PasswordException if the old password is incorrect or two new passwords don't match
     */
    public void changePassword(byte[] oldPassword, byte[] newPassword, byte[] confirmNewPassword)
            throws IOException, GeneralSecurityException, PasswordException {
        changePassword(oldPassword, newPassword, confirmNewPassword, new StatusListener() {
            public void updateStatus(String status) {
            } // Do nothing
        });
    }

    /**
     * Reencrypts all encrypted files with a new password
     * @param oldPassword
     * @param newPassword
     * @param confirmNewPassword
     * @param lsnr A StatusListener to report progress to
     * @throws IOException 
     * @throws GeneralSecurityException 
     * @throws PasswordException if the old password is incorrect or two new passwords don't match
     */
    public void changePassword(byte[] oldPassword, byte[] newPassword, byte[] confirmNewPassword,
            StatusListener lsnr) throws IOException, GeneralSecurityException, PasswordException {
        File passwordFile = configuration.getPasswordFile();

        lsnr.updateStatus(_t("Checking password"));

        if (!FileEncryptionUtil.isPasswordCorrect(oldPassword, passwordFile))
            throw new PasswordException(_t("The old password is not correct."));
        if (!Arrays.equals(newPassword, confirmNewPassword))
            throw new PasswordException(_t("The new password and the confirmation password do not match."));

        // lock so no files are encrypted with the old password while the password is being changed
        synchronized (passwordCache) {
            passwordCache.setPassword(newPassword);
            DerivedKey newKey = passwordCache.getKey();

            lsnr.updateStatus(_t("Re-encrypting identities"));
            identities.changePassword(oldPassword, newKey);

            lsnr.updateStatus(_t("Re-encrypting addressbook"));
            addressBook.changePassword(oldPassword, newKey);
            for (EmailFolder folder : getEmailFolders()) {
                lsnr.updateStatus(_t("Re-encrypting folder") + " " + folder.getName());
                folder.changePassword(oldPassword, newKey);
            }

            lsnr.updateStatus(_t("Updating password file"));
            FileEncryptionUtil.writePasswordFile(passwordFile, passwordCache.getPassword(), newKey);
        }
    }

    /**
     * Tests if a password is correct and stores it in the cache if it is.
     * If the password is not correct, a <code>PasswordException</code> is thrown.
     * @param password
     * @throws IOException
     * @throws GeneralSecurityException
     * @throws PasswordException
     */
    @Override
    public void tryPassword(byte[] password) throws IOException, GeneralSecurityException, PasswordException {
        File passwordFile = configuration.getPasswordFile();
        boolean correct = FileEncryptionUtil.isPasswordCorrect(password, passwordFile);
        if (correct) {
            // Don't cache tried password if none is set. This check is needed
            // because IMAP doesn't support a blank password, so the user
            // inputs a random string.
            if (passwordFile.exists())
                passwordCache.setPassword(password);
        } else
            throw new PasswordException();
    }

    /** Returns <code>true</code> if the password is currently cached. */
    public boolean isPasswordInCache() {
        return passwordCache.isPasswordInCache();
    }

    /**
     * Returns <code>true</code> if a password is set but is not currently cached;
     * <code>false</code> otherwise.
     */
    public boolean isPasswordRequired() {
        return passwordCache.getPassword() == null;
    }

    /** Removes the password from the password cache. If there is no password in the cache, nothing happens. */
    public void clearPassword() {
        passwordCache.clear();
    }

    public void addPasswordCacheListener(PasswordCacheListener passwordCacheListener) {
        passwordCache.addPasswordCacheListener(passwordCacheListener);
    }

    public void removePasswordCacheListener(PasswordCacheListener passwordCacheListener) {
        passwordCache.removePasswordCacheListener(passwordCacheListener);
    }

    public List<File> getUndecryptableFiles() throws PasswordException, IOException, GeneralSecurityException {
        return debugSupport.getUndecryptableFiles();
    }

    public List<EmailFolder> getEmailFolders() {
        ArrayList<EmailFolder> folders = new ArrayList<EmailFolder>();
        folders.add(inbox);
        folders.add(outbox);
        folders.add(sentFolder);
        folders.add(trashFolder);
        return folders;
    }

    public DhtPeerStats getDhtStats() {
        if (dht == null)
            return null;
        else
            return dht.getPeerStats();
    }

    public Set<RelayPeer> getRelayPeers() {
        if (peerManager == null)
            return new HashSet<RelayPeer>();
        return peerManager.getAllPeers();
    }

    public Collection<BannedPeer> getBannedPeers() {
        return BanList.getInstance().getAll();
    }

    private void startAllServices() {
        for (I2PAppThread thread : backgroundThreads)
            if (thread != null && thread.getState() == State.NEW) // the check for State.NEW is only there for ConnectTask
                thread.start();
    }

    private void stopAllServices() {
        if (backgroundThreads != null) {
            // interrupt all threads
            for (I2PAppThread thread : backgroundThreads)
                if (thread != null && thread.isAlive())
                    thread.interrupt();
        }
        stopImap();
        stopSmtp();
        if (backgroundThreads != null) {
            awaitShutdown(5 * 1000);
            printRunningThreads();
        }
    }

    private void printRunningThreads() {
        List<Thread> runningThreads = new ArrayList<Thread>();
        for (Thread thread : backgroundThreads)
            if (thread.isAlive())
                runningThreads.add(thread);
        log.debug(runningThreads.size() + " threads still running 5 seconds after interrupt()"
                + (runningThreads.isEmpty() ? '.' : ':'));
        for (Thread thread : runningThreads)
            log.debug("  " + thread.getName());
        if (imapService != null && imapService.isStarted())
            log.debug("IMAP service still running");
        if (smtpService != null && smtpService.isRunning())
            log.debug("SMTP service still running");
    }

    /**
     * Waits up to <code>timeout</code> milliseconds for the background threads to end.
     * @param timeout In milliseconds
     */
    private void awaitShutdown(long timeout) {
        long deadline = System.currentTimeMillis() + timeout; // the time at which any background threads that are still running are interrupted

        for (I2PAppThread thread : backgroundThreads)
            if (thread != null)
                try {
                    long remainingTime = deadline - System.currentTimeMillis(); // the time until the original timeout
                    if (remainingTime < 0)
                        return;
                    thread.join(remainingTime);
                } catch (InterruptedException e) {
                    log.error("Interrupted while waiting for thread <" + thread.getName() + "> to exit", e);
                    return;
                }
    }

    /**
     * Connects to the network, skipping the connect delay.<br/>
     * If the delay time has already passed, calling this method has no effect.
     */
    public void connectNow() {
        connectTask.startSignal.countDown();
    }

    public void networkStatusChanged() {
        synchronized (networkStatusListeners) {
            for (NetworkStatusListener nsl : networkStatusListeners)
                nsl.networkStatusChanged();
        }
    }

    @Override
    public void addNetworkStatusListener(NetworkStatusListener networkStatusListener) {
        synchronized (networkStatusListeners) {
            networkStatusListeners.add(networkStatusListener);
        }
    }

    @Override
    public void removeNetworkStatusListener(NetworkStatusListener networkStatusListener) {
        synchronized (networkStatusListeners) {
            networkStatusListeners.remove(networkStatusListener);
        }
    }

    @Override
    public NetworkStatus getNetworkStatus() {
        if (connectTask == null)
            return NetworkStatus.NOT_STARTED;
        if (!connectTask.isDone())
            return connectTask.getNetworkStatus();
        else if (dht != null)
            return dht.isReady() ? NetworkStatus.CONNECTED : NetworkStatus.CONNECTING;
        else
            return NetworkStatus.ERROR;
    }

    @Override
    public Exception getConnectError() {
        return connectTask.getError();
    }

    @Override
    public boolean isConnected() {
        return getNetworkStatus() == NetworkStatus.CONNECTED;
    }

    /**
     * Waits <code>STARTUP_DELAY</code> milliseconds or until <code>startSignal</code>
     * is triggered from outside this class, then sets up an I2P session and everything
     * that depends on it.
     */
    private class ConnectTask extends I2PAppThread {
        volatile NetworkStatus status = NetworkStatus.NOT_STARTED;
        volatile Exception error;
        CountDownLatch startSignal = new CountDownLatch(1);
        CountDownLatch doneSignal = new CountDownLatch(1);

        protected ConnectTask() {
            super("ConnectTask");
            setDaemon(true);
        }

        public NetworkStatus getNetworkStatus() {
            return status;
        }

        public Exception getError() {
            return error;
        }

        public boolean isDone() {
            try {
                return doneSignal.await(0, TimeUnit.SECONDS);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                return false;
            }
        }

        @Override
        public void run() {
            status = NetworkStatus.DELAY;
            networkStatusChanged();
            try {
                startSignal.await(STARTUP_DELAY, TimeUnit.MINUTES);
                status = NetworkStatus.CONNECTING;
                networkStatusChanged();
                initializeSession();
                initializeServices();
                startAllServices();
                doneSignal.countDown();
                networkStatusChanged();
            } catch (InterruptedException e) {
                log.debug("ConnectTask interrupted, exiting");
            } catch (Exception e) {
                status = NetworkStatus.ERROR;
                networkStatusChanged();
                error = e;
                log.error("Can't initialize the application.", e);
            }
        }
    }
}