forked from I2P_Developers/i2p.i2p
- Add background mail checker, not yet enabled - Add idle timeout connection closer - Rely on idle checker for most delayed deletions - Cleanup resources better when shutting down session - Don't add deleted mails to folder, caused errors after deletions - Set socket soTimeouts so things don't hang forever - Display errors after check mail button pushed - More IOE debug logging
282 lines
7.3 KiB
Java
282 lines
7.3 KiB
Java
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.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 {
|
|
|
|
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 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-~";
|
|
|
|
/**
|
|
* Use the params to generate a unique directory name.
|
|
* @param pass ignored
|
|
*/
|
|
public PersistentMailCache(String host, int port, String user, String pass) throws IOException {
|
|
_cacheDir = makeCacheDirs(host, port, user, pass);
|
|
// TODO static locking
|
|
}
|
|
|
|
/**
|
|
* Fetch all mails from disk.
|
|
*
|
|
* @return a new collection
|
|
*/
|
|
public Collection<Mail> 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) {
|
|
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) {
|
|
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) {
|
|
getFullFile(uidl).delete();
|
|
getHeaderFile(uidl).delete();
|
|
}
|
|
|
|
/**
|
|
* ~/.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);
|
|
int read = 0;
|
|
byte buf[] = new byte[4*1024];
|
|
while ( (read = in.read(buf)) != -1) {
|
|
out.write(buf, 0, read);
|
|
}
|
|
ReadBuffer rb = new ReadBuffer(out.toByteArray(), 0, out.size());
|
|
return rb;
|
|
} catch (IOException ioe) {
|
|
Debug.debug(Debug.ERROR, "Error reading: " + f + ": " + ioe);
|
|
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;
|
|
}
|
|
}
|