package i2p.susi.webmail;
import i2p.susi.debug.Debug;
import i2p.susi.webmail.Messages;
import i2p.susi.util.ReadBuffer;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Hashtable;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;
import net.i2p.I2PAppContext;
import net.i2p.data.Base64;
import net.i2p.data.DataHelper;
import net.i2p.util.PasswordManager;
import net.i2p.util.SecureDirectory;
import net.i2p.util.SecureFile;
import net.i2p.util.SecureFileOutputStream;
/**
* Manage the on-disk cache.
*
* This is a custom format with subdirectories, gzipped files,
* and the encoded UIDL in the file name.
* We store either the headers or the full message.
* No, it is not Maildir format but we could add Maildir-style
* status suffixes (e.g. ":2.SR") later.
*
* Exporting to a Maildir format would be just ungzipping
* each file to a flat directory.
*
* TODO draft and sent folders, cached server caps and config.
*
* @since 0.9.14
*/
class PersistentMailCache {
/**
* One lock for each user in the whole JVM, to protect against multiple sessions.
* One big lock for the whole cache dir, not one for each file or subdir.
* Never expired.
* Sure, if we did a maildir format we wouldn't need this.
*/
private static final ConcurrentHashMap<String, Object> _locks = new ConcurrentHashMap<String, Object>();
private final Object _lock;
private final File _cacheDir;
private static final String DIR_SUSI = "susimail";
private static final String DIR_CACHE = "cache";
private static final String CACHE_PREFIX = "cache-";
private static final String DIR_FOLDER = "cur"; // MailDir-like
private static final String DIR_PREFIX = "s";
private static final String FILE_PREFIX = "mail-";
private static final String HDR_SUFFIX = ".hdr.txt.gz";
private static final String FULL_SUFFIX = ".full.txt.gz";
private static final String B64 = Base64.ALPHABET_I2P;
/**
* Use the params to generate a unique directory name.
* @param pass ignored
*/
public PersistentMailCache(String host, int port, String user, String pass) throws IOException {
_lock = getLock(host, port, user, pass);
synchronized(_lock) {
_cacheDir = makeCacheDirs(host, port, user, pass);
}
}
/**
* Fetch all mails from disk.
*
* @return a new collection
*/
public Collection<Mail> getMails() {
synchronized(_lock) {
return locked_getMails();
}
}
private Collection<Mail> locked_getMails() {
List<Mail> rv = new ArrayList<Mail>();
for (int j = 0; j < B64.length(); j++) {
File subdir = new File(_cacheDir, DIR_PREFIX + B64.charAt(j));
File[] files = subdir.listFiles();
if (files == null)
continue;
for (int i = 0; i < files.length; i++) {
File f = files[i];
if (!f.isFile())
continue;
Mail mail = load(f);
if (mail != null)
rv.add(mail);
}
}
return rv;
}
/**
* Fetch any needed data from disk.
*
* @return success
*/
public boolean getMail(Mail mail, boolean headerOnly) {
synchronized(_lock) {
return locked_getMail(mail, headerOnly);
}
}
private boolean locked_getMail(Mail mail, boolean headerOnly) {
File f = getFullFile(mail.uidl);
if (f.exists()) {
ReadBuffer rb = read(f);
if (rb != null) {
mail.setBody(rb);
return true;
}
}
f = getHeaderFile(mail.uidl);
if (f.exists()) {
ReadBuffer rb = read(f);
if (rb != null) {
mail.setHeader(rb);
return true;
}
}
return false;
}
/**
* Save data to disk.
*
* @return success
*/
public boolean saveMail(Mail mail) {
synchronized(_lock) {
return locked_saveMail(mail);
}
}
private boolean locked_saveMail(Mail mail) {
ReadBuffer rb = mail.getBody();
if (rb != null) {
File f = getFullFile(mail.uidl);
if (f.exists())
return true; // already there, all good
boolean rv = write(rb, f);
if (rv)
getHeaderFile(mail.uidl).delete();
return rv;
}
rb = mail.getHeader();
if (rb != null) {
File f = getHeaderFile(mail.uidl);
if (f.exists())
return true; // already there, all good
boolean rv = write(rb, f);
return rv;
}
return false;
}
/**
*
* Delete data from disk.
*/
public void deleteMail(Mail mail) {
deleteMail(mail.uidl);
}
/**
*
* Delete data from disk.
*/
public void deleteMail(String uidl) {
synchronized(_lock) {
getFullFile(uidl).delete();
getHeaderFile(uidl).delete();
}
}
private static Object getLock(String host, int port, String user, String pass) {
Object lock = new Object();
Object old = _locks.putIfAbsent(user + host + port, lock);
return (old != null) ? old : lock;
}
/**
* ~/.i2p/susimail/cache/cache-xxxxx/cur/s[a-z]/mail-xxxxx.full.txt.gz
* folder1 is the base.
*/
private static File makeCacheDirs(String host, int port, String user, String pass) throws IOException {
File f = new SecureDirectory(I2PAppContext.getGlobalContext().getConfigDir(), DIR_SUSI);
if (!f.exists() && !f.mkdir())
throw new IOException("Cannot create " + f);
f = new SecureDirectory(f, DIR_CACHE);
if (!f.exists() && !f.mkdir())
throw new IOException("Cannot create " + f);
f = new SecureDirectory(f, CACHE_PREFIX + Base64.encode(user + host + port));
if (!f.exists() && !f.mkdir())
throw new IOException("Cannot create " + f);
File base = new SecureDirectory(f, DIR_FOLDER);
if (!base.exists() && !base.mkdir())
throw new IOException("Cannot create " + base);
for (int i = 0; i < B64.length(); i++) {
f = new SecureDirectory(base, DIR_PREFIX + B64.charAt(i));
if (!f.exists() && !f.mkdir())
throw new IOException("Cannot create " + f);
}
return base;
}
private File getHeaderFile(String uidl) {
return getFile(uidl, HDR_SUFFIX);
}
private File getFullFile(String uidl) {
return getFile(uidl, FULL_SUFFIX);
}
private File getFile(String uidl, String suffix) {
byte[] raw = DataHelper.getASCII(uidl);
byte[] md5 = PasswordManager.md5Sum(raw);
String db64 = Base64.encode(md5);
File dir = new File(_cacheDir, DIR_PREFIX + db64.charAt(0));
String b64 = Base64.encode(uidl);
return new SecureFile(dir, FILE_PREFIX + b64 + suffix);
}
/**
* Save data to disk.
*
* @return success
*/
private static boolean write(ReadBuffer rb, File f) {
OutputStream out = null;
try {
out = new BufferedOutputStream(new GZIPOutputStream(new SecureFileOutputStream(f)));
out.write(rb.content, rb.offset, rb.length);
return true;
} catch (IOException ioe) {
Debug.debug(Debug.ERROR, "Error writing: " + f + ": " + ioe);
return false;
} finally {
if (out != null)
try { out.close(); } catch (IOException ioe) {}
}
}
/**
* @return null on failure
*/
private static ReadBuffer read(File f) {
InputStream in = null;
try {
long len = f.length();
if (len > 16 * 1024 * 1024) {
throw new IOException("too big");
}
in = new GZIPInputStream(new BufferedInputStream(new FileInputStream(f)));
ByteArrayOutputStream out = new ByteArrayOutputStream((int) len);
DataHelper.copy(in, out);
ReadBuffer rb = new ReadBuffer(out.toByteArray(), 0, out.size());
return rb;
} catch (IOException ioe) {
Debug.debug(Debug.ERROR, "Error reading: " + f + ": " + ioe);
return null;
} catch (OutOfMemoryError oom) {
Debug.debug(Debug.ERROR, "Error reading: " + f + ": " + oom);
return null;
} finally {
if (in != null)
try { in.close(); } catch (IOException ioe) {}
}
}
/**
* @return null on failure
*/
private static Mail load(File f) {
String name = f.getName();
String uidl;
boolean headerOnly;
if (name.endsWith(FULL_SUFFIX)) {
uidl= Base64.decodeToString(name.substring(FILE_PREFIX.length(), name.length() - FULL_SUFFIX.length()));
headerOnly = false;
} else if (name.endsWith(HDR_SUFFIX)) {
uidl= Base64.decodeToString(name.substring(FILE_PREFIX.length(), name.length() - HDR_SUFFIX.length()));
headerOnly = true;
} else {
return null;
}
if (uidl == null)
return null;
ReadBuffer rb = read(f);
if (rb == null)
return null;
Mail mail = new Mail(uidl);
if (headerOnly)
mail.setHeader(rb);
else
mail.setBody(rb);
return mail;
}
}