forked from I2P_Developers/i2p.i2p
SusiMail: Add folders, drafts, background sending (ticket #2087)
Use with caution; cleanups and CSS to follow
This commit is contained in:
@@ -86,6 +86,6 @@ public class MemoryBuffer implements Buffer {
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "SB " + (content == null ? "empty" : content.length + " bytes");
|
||||
return "MB " + (content == null ? "empty" : content.length + " bytes");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,6 +72,14 @@ public class Attachment {
|
||||
return new FileInputStream(data);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return absolute path to the data file
|
||||
* @since 0.9.35
|
||||
*/
|
||||
public String getPath() {
|
||||
return data.getAbsolutePath();
|
||||
}
|
||||
|
||||
/**
|
||||
* The unencoded size
|
||||
* @since 0.9.33
|
||||
|
||||
136
apps/susimail/src/src/i2p/susi/webmail/Draft.java
Normal file
136
apps/susimail/src/src/i2p/susi/webmail/Draft.java
Normal file
@@ -0,0 +1,136 @@
|
||||
package i2p.susi.webmail;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import net.i2p.data.Base64;
|
||||
import net.i2p.data.DataHelper;
|
||||
|
||||
import i2p.susi.util.Buffer;
|
||||
import i2p.susi.webmail.encoding.Encoding;
|
||||
import i2p.susi.webmail.encoding.EncodingException;
|
||||
import i2p.susi.webmail.encoding.EncodingFactory;
|
||||
|
||||
/**
|
||||
* Holds a draft message and reference to attachments, if any
|
||||
*
|
||||
* Differences from Mail:
|
||||
* - Never multipart, body is always text/plain UTF-8
|
||||
* - Attachments are just headers containing name, type, encoding, and path to file
|
||||
* - Bcc is a header
|
||||
*
|
||||
* @since 0.9.35
|
||||
*/
|
||||
class Draft extends Mail {
|
||||
|
||||
private final List<Attachment> attachments;
|
||||
String[] bcc; // addresses only, enclosed by <>
|
||||
private static final String HDR_ATTACH = "X-I2P-Attachment: ";
|
||||
public static final String HDR_BCC = "Bcc: ";
|
||||
|
||||
public Draft(String uidl) {
|
||||
super(uidl);
|
||||
attachments = new ArrayList<Attachment>(4);
|
||||
}
|
||||
|
||||
/**
|
||||
* Overridden to process attachment and Bcc headers
|
||||
*/
|
||||
@Override
|
||||
public synchronized void setBody(Buffer rb) {
|
||||
super.setBody(rb);
|
||||
MailPart part = getPart();
|
||||
if (part != null) {
|
||||
String[] hdrs = part.headerLines;
|
||||
for (int i = 0; i < hdrs.length; i++) {
|
||||
String hdr = hdrs[i];
|
||||
if (hdr.startsWith(HDR_BCC)) {
|
||||
ArrayList<String> list = new ArrayList<String>();
|
||||
getRecipientsFromList(list, hdr.substring(HDR_BCC.length()).trim(), true);
|
||||
if (list.isEmpty()) {
|
||||
// don't set
|
||||
} else if (bcc == null) {
|
||||
bcc = list.toArray(new String[list.size()]);
|
||||
} else {
|
||||
// add to the array, shouldn't happen
|
||||
for (int j = 0; j < bcc.length; j++) {
|
||||
list.add(j, bcc[i]);
|
||||
}
|
||||
bcc = list.toArray(new String[list.size()]);
|
||||
}
|
||||
}
|
||||
if (!hdr.startsWith(HDR_ATTACH))
|
||||
break;
|
||||
String[] flds = DataHelper.split(hdr.substring(HDR_ATTACH.length()), ",", 4);
|
||||
if (flds.length != 4)
|
||||
continue;
|
||||
byte[] b = Base64.decode(flds[0]);
|
||||
if (b == null)
|
||||
continue;
|
||||
String name = DataHelper.getUTF8(b);
|
||||
String type = flds[1];
|
||||
String enc = flds[2];
|
||||
b = Base64.decode(flds[0]);
|
||||
if (b == null)
|
||||
continue;
|
||||
String path = DataHelper.getUTF8(b);
|
||||
attachments.add(new Attachment(name, type, enc, new File(path)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized boolean hasAttachment() {
|
||||
return !attachments.isEmpty();
|
||||
}
|
||||
|
||||
/** @return may be null */
|
||||
public synchronized String[] getBcc() {
|
||||
return bcc;
|
||||
}
|
||||
|
||||
/** @return non-null, not a copy */
|
||||
public synchronized List<Attachment> getAttachments() {
|
||||
return attachments;
|
||||
}
|
||||
|
||||
public synchronized int addAttachment(Attachment a) {
|
||||
int rv = attachments.indexOf(a);
|
||||
if (rv >= 0)
|
||||
return rv;
|
||||
rv = attachments.size();
|
||||
attachments.add(a);
|
||||
return rv;
|
||||
}
|
||||
|
||||
public synchronized void removeAttachment(int index) {
|
||||
if (index >= 0 && index < attachments.size()) {
|
||||
Attachment a = attachments.get(index);
|
||||
a.deleteData();
|
||||
attachments.remove(index);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized void clearAttachments() {
|
||||
for (Attachment a : attachments) {
|
||||
a.deleteData();
|
||||
}
|
||||
attachments.clear();
|
||||
}
|
||||
|
||||
public synchronized StringBuilder encodeAttachments() {
|
||||
StringBuilder buf = new StringBuilder(256 * attachments.size());
|
||||
if (attachments.isEmpty())
|
||||
return buf;
|
||||
for (int i = 0; i < attachments.size(); i++) {
|
||||
Attachment a = attachments.get(i);
|
||||
buf.append(HDR_ATTACH);
|
||||
buf.append(Base64.encode(a.getFileName())).append(',');
|
||||
buf.append(a.getContentType()).append(',');
|
||||
buf.append(a.getTransferEncoding()).append(',');
|
||||
buf.append(Base64.encode(a.getPath())).append("\r\n");
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
}
|
||||
@@ -300,6 +300,23 @@ class Mail {
|
||||
{
|
||||
if( text != null && text.length() > 0 ) {
|
||||
String[] ccs = DataHelper.split(text, ",");
|
||||
ok = getRecipientsFromList(recipients, ccs, ok);
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
/**
|
||||
* A little misnamed. Adds all addresses from the elements
|
||||
* in text to the recipients list.
|
||||
*
|
||||
* @param recipients out param
|
||||
* @param ok will be returned
|
||||
* @return true if ALL e-mail addresses are valid AND the in parameter was true
|
||||
* @since 0.9.35
|
||||
*/
|
||||
public static boolean getRecipientsFromList( ArrayList<String> recipients, String[] ccs, boolean ok )
|
||||
{
|
||||
if (ccs != null && ccs.length > 0 ) {
|
||||
for( int i = 0; i < ccs.length; i++ ) {
|
||||
String recipient = ccs[i].trim();
|
||||
if( validateAddress( recipient ) ) {
|
||||
|
||||
@@ -27,8 +27,11 @@ import i2p.susi.debug.Debug;
|
||||
import i2p.susi.util.Config;
|
||||
import i2p.susi.util.Buffer;
|
||||
import i2p.susi.util.FileBuffer;
|
||||
import i2p.susi.util.Folder;
|
||||
import i2p.susi.util.Folder.SortOrder;
|
||||
import i2p.susi.util.ReadBuffer;
|
||||
import i2p.susi.util.MemoryBuffer;
|
||||
import static i2p.susi.webmail.Sorters.*;
|
||||
import i2p.susi.webmail.pop3.POP3MailBox;
|
||||
import i2p.susi.webmail.pop3.POP3MailBox.FetchRequest;
|
||||
|
||||
@@ -43,9 +46,13 @@ import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import net.i2p.I2PAppContext;
|
||||
import net.i2p.util.FileUtil;
|
||||
import net.i2p.util.I2PAppThread;
|
||||
|
||||
/**
|
||||
* There's one of these for each Folder.
|
||||
* However, only DIR_FOLDER has a non-null POP3MailBox.
|
||||
*
|
||||
* @author user
|
||||
*/
|
||||
class MailCache {
|
||||
@@ -58,7 +65,11 @@ class MailCache {
|
||||
private final Hashtable<String, Mail> mails;
|
||||
private final PersistentMailCache disk;
|
||||
private final I2PAppContext _context;
|
||||
private final Folder<String> folder;
|
||||
private final String folderName;
|
||||
private NewMailListener _loadInProgress;
|
||||
private boolean _isLoaded;
|
||||
private final boolean _isDrafts;
|
||||
|
||||
/** Includes header, headers are generally 1KB to 1.5 KB,
|
||||
* and bodies will compress well.
|
||||
@@ -69,15 +80,156 @@ class MailCache {
|
||||
/**
|
||||
* Does NOT load the mails in. Caller MUST call loadFromDisk().
|
||||
*
|
||||
* @param mailbox non-null
|
||||
* @param mailbox non-null for DIR_FOLDER; null otherwise
|
||||
*/
|
||||
MailCache(I2PAppContext ctx, POP3MailBox mailbox,
|
||||
MailCache(I2PAppContext ctx, POP3MailBox mailbox, String folderName,
|
||||
String host, int port, String user, String pass) throws IOException {
|
||||
this.mailbox = mailbox;
|
||||
mails = new Hashtable<String, Mail>();
|
||||
disk = new PersistentMailCache(ctx, host, port, user, pass, PersistentMailCache.DIR_FOLDER);
|
||||
disk = new PersistentMailCache(ctx, host, port, user, pass, folderName);
|
||||
// TODO Drafts, Sent, Trash
|
||||
_context = ctx;
|
||||
Folder<String> folder = new Folder<String>();
|
||||
// setElements() sorts, so configure the sorting first
|
||||
//sessionObject.folder.addSorter( SORT_ID, new IDSorter( sessionObject.mailCache ) );
|
||||
folder.addSorter(WebMail.SORT_SENDER, new SenderSorter(this));
|
||||
folder.addSorter(WebMail.SORT_SUBJECT, new SubjectSorter(this));
|
||||
folder.addSorter(WebMail.SORT_DATE, new DateSorter(this));
|
||||
folder.addSorter(WebMail.SORT_SIZE, new SizeSorter(this));
|
||||
// reverse sort, latest mail first
|
||||
// TODO get user defaults from config
|
||||
folder.setSortBy(WebMail.SORT_DEFAULT, WebMail.SORT_ORDER_DEFAULT);
|
||||
this.folder = folder;
|
||||
this.folderName = folderName;
|
||||
_isDrafts = folderName.equals(WebMail.DIR_DRAFTS);
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 0.9.35
|
||||
* @return as passed in
|
||||
*/
|
||||
public String getFolderName() {
|
||||
return folderName;
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 0.9.35
|
||||
* @return translation of name passed in
|
||||
*/
|
||||
public String getTranslatedName() {
|
||||
String rv = folderName.equals(WebMail.DIR_FOLDER) ? "Inbox" : folderName;
|
||||
return Messages.getString(rv);
|
||||
}
|
||||
|
||||
/**
|
||||
* @since 0.9.35
|
||||
* @return non-null
|
||||
*/
|
||||
public Folder<String> getFolder() {
|
||||
return folder;
|
||||
}
|
||||
|
||||
/**
|
||||
* For writing a new full mail (NOT headers only)
|
||||
* Caller must close.
|
||||
* @since 0.9.35
|
||||
*/
|
||||
public Buffer getFullWriteBuffer(String uidl) {
|
||||
// no locking this way
|
||||
return disk.getFullBuffer(uidl);
|
||||
}
|
||||
|
||||
/**
|
||||
* For writing a new full mail
|
||||
* @param buffer as received from getFullBuffer
|
||||
* @since 0.9.35
|
||||
*/
|
||||
public void writeComplete(String uidl, Buffer buffer, boolean success) {
|
||||
buffer.writeComplete(success);
|
||||
if (success) {
|
||||
Mail mail;
|
||||
if (_isDrafts)
|
||||
mail = new Draft(uidl);
|
||||
else
|
||||
mail = new Mail(uidl);
|
||||
mail.setBody(buffer);
|
||||
synchronized(mails) {
|
||||
mails.put(uidl, mail);
|
||||
}
|
||||
folder.addElement(uidl);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return non-null only for Drafts
|
||||
* @since 0.9.35
|
||||
*/
|
||||
public File getAttachmentDir() {
|
||||
return disk.getAttachmentDir();
|
||||
}
|
||||
|
||||
/**
|
||||
* Move a mail to another MailCache, neither may be DIR_DRAFTS
|
||||
* @return success
|
||||
* @since 0.9.35
|
||||
*/
|
||||
public boolean moveTo(String uidl, MailCache toMC) {
|
||||
if (folderName.equals(WebMail.DIR_DRAFTS) ||
|
||||
toMC.getFolderName().equals(WebMail.DIR_DRAFTS))
|
||||
return false;
|
||||
Mail mail;
|
||||
synchronized(mails) {
|
||||
mail = mails.get(uidl);
|
||||
if (mail == null)
|
||||
return false;
|
||||
if (!mail.hasBody())
|
||||
return false;
|
||||
File from = disk.getFullFile(uidl);
|
||||
if (!from.exists())
|
||||
return false;
|
||||
PersistentMailCache toPMC = toMC.disk;
|
||||
File to = toPMC.getFullFile(uidl);
|
||||
if (to.exists())
|
||||
return false;
|
||||
if (!FileUtil.rename(from, to))
|
||||
return false;
|
||||
mails.remove(uidl);
|
||||
folder.removeElement(uidl);
|
||||
}
|
||||
toMC.movedTo(mail);
|
||||
if (mailbox != null)
|
||||
mailbox.queueForDeletion(mail.uidl);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Moved a mail from another MailCache
|
||||
* @since 0.9.35
|
||||
*/
|
||||
private void movedTo(Mail mail) {
|
||||
synchronized(mails) {
|
||||
// we must reset the body of the mail to the new FileBuffer
|
||||
Buffer body = disk.getFullBuffer(mail.uidl);
|
||||
mail.setBody(body);
|
||||
mails.put(mail.uidl, mail);
|
||||
}
|
||||
folder.addElement(mail.uidl);
|
||||
}
|
||||
|
||||
/**
|
||||
* Is loadFromDisk in progress?
|
||||
* @since 0.9.35
|
||||
*/
|
||||
public synchronized boolean isLoading() {
|
||||
return _loadInProgress != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Has loadFromDisk completed?
|
||||
* @since 0.9.35
|
||||
*/
|
||||
public synchronized boolean isLoaded() {
|
||||
return _isLoaded;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -88,8 +240,9 @@ class MailCache {
|
||||
* @since 0.9.13
|
||||
*/
|
||||
public synchronized boolean loadFromDisk(NewMailListener nml) {
|
||||
if (_loadInProgress != null)
|
||||
if (_isLoaded || _loadInProgress != null)
|
||||
return false;
|
||||
Debug.debug(Debug.DEBUG, "Loading folder " + folderName);
|
||||
Thread t = new I2PAppThread(new LoadMailRunner(nml), "Email loader");
|
||||
_loadInProgress = nml;
|
||||
try {
|
||||
@@ -115,10 +268,12 @@ class MailCache {
|
||||
blockingLoadFromDisk();
|
||||
if(!mails.isEmpty())
|
||||
result = true;
|
||||
Debug.debug(Debug.DEBUG, "Folder loaded: " + folderName);
|
||||
} finally {
|
||||
synchronized(MailCache.this) {
|
||||
if (_loadInProgress == _nml)
|
||||
_loadInProgress = null;
|
||||
_isLoaded = true;
|
||||
}
|
||||
_nml.foundNewMail(result);
|
||||
}
|
||||
@@ -133,7 +288,7 @@ class MailCache {
|
||||
*/
|
||||
private void blockingLoadFromDisk() {
|
||||
synchronized(mails) {
|
||||
if (!mails.isEmpty())
|
||||
if (_isLoaded)
|
||||
throw new IllegalStateException();
|
||||
Collection<Mail> dmails = disk.getMails();
|
||||
for (Mail mail : dmails) {
|
||||
@@ -164,7 +319,8 @@ class MailCache {
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch any needed data from pop3 server, unless mode is CACHE_ONLY.
|
||||
* Fetch any needed data from pop3 server, unless mode is CACHE_ONLY,
|
||||
* or this isn't the Inbox.
|
||||
* Blocking unless mode is CACHE_ONLY.
|
||||
*
|
||||
* @param uidl message id to get
|
||||
@@ -173,7 +329,6 @@ class MailCache {
|
||||
*/
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
public Mail getMail(String uidl, FetchMode mode) {
|
||||
|
||||
Mail mail = null, newMail = null;
|
||||
|
||||
/*
|
||||
@@ -182,6 +337,9 @@ class MailCache {
|
||||
synchronized(mails) {
|
||||
mail = mails.get( uidl );
|
||||
if( mail == null ) {
|
||||
// if not in inbox, we can't fetch, this is what we have
|
||||
if (mailbox == null)
|
||||
return null;
|
||||
newMail = new Mail(uidl);
|
||||
// TODO really?
|
||||
mails.put( uidl, newMail );
|
||||
@@ -193,13 +351,20 @@ class MailCache {
|
||||
}
|
||||
if (mail.markForDeletion)
|
||||
return null;
|
||||
// if not in inbox, we can't fetch, this is what we have
|
||||
if (mailbox == null)
|
||||
return mail;
|
||||
|
||||
long sz = mail.getSize();
|
||||
if (mode == FetchMode.HEADER && sz > 0 && sz <= FETCH_ALL_SIZE)
|
||||
mode = FetchMode.ALL;
|
||||
|
||||
if (mode == FetchMode.HEADER) {
|
||||
if(!mail.hasHeader())
|
||||
mail.setHeader(mailbox.getHeader(uidl));
|
||||
if (!mail.hasHeader()) {
|
||||
Buffer buf = mailbox.getHeader(uidl);
|
||||
if (buf != null)
|
||||
mail.setHeader(buf);
|
||||
}
|
||||
} else if (mode == FetchMode.ALL) {
|
||||
if(!mail.hasBody()) {
|
||||
File file = new File(_context.getTempDir(), "susimail-new-" + _context.random().nextLong());
|
||||
@@ -223,6 +388,7 @@ class MailCache {
|
||||
* Mail objects are inserted into the requests.
|
||||
* After this, call getUIDLs() to get all known mail UIDLs.
|
||||
* MUST already be connected, otherwise returns false.
|
||||
* Call only on inbox!
|
||||
*
|
||||
* Blocking.
|
||||
*
|
||||
@@ -234,6 +400,10 @@ class MailCache {
|
||||
public boolean getMail(FetchMode mode) {
|
||||
if (mode == FetchMode.CACHE_ONLY)
|
||||
throw new IllegalArgumentException();
|
||||
if (mailbox == null) {
|
||||
Debug.debug(Debug.DEBUG, "getMail() mode " + mode + " called on wrong folder " + getFolderName(), new Exception());
|
||||
return false;
|
||||
}
|
||||
boolean hOnly = mode == FetchMode.HEADER;
|
||||
|
||||
Collection<String> popKnown = mailbox.getUIDLs();
|
||||
@@ -348,6 +518,7 @@ class MailCache {
|
||||
* Mark mail for deletion locally.
|
||||
* Send delete requests to POP3 then quit and reconnect.
|
||||
* No success/failure indication is returned.
|
||||
* Does not delete from folder.
|
||||
*
|
||||
* @since 0.9.13
|
||||
*/
|
||||
@@ -359,6 +530,7 @@ class MailCache {
|
||||
* Mark mail for deletion locally.
|
||||
* Send delete requests to POP3 then quit and reconnect.
|
||||
* No success/failure indication is returned.
|
||||
* Does not delete from folder.
|
||||
*
|
||||
* @since 0.9.13
|
||||
*/
|
||||
@@ -381,7 +553,8 @@ class MailCache {
|
||||
}
|
||||
if (toDelete.isEmpty())
|
||||
return;
|
||||
mailbox.queueForDeletion(toDelete);
|
||||
if (mailbox != null)
|
||||
mailbox.queueForDeletion(toDelete);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -301,10 +301,12 @@ class MailPart {
|
||||
}
|
||||
|
||||
/**
|
||||
* Synched because FileBuffer keeps stream open
|
||||
*
|
||||
* @param offset 2 for sendAttachment, 0 otherwise, probably for \r\n
|
||||
* @since 0.9.13
|
||||
*/
|
||||
public void decode(int offset, Buffer out) throws IOException {
|
||||
public synchronized void decode(int offset, Buffer out) throws IOException {
|
||||
String encg = encoding;
|
||||
if (encg == null) {
|
||||
//throw new DecodingException("No encoding specified");
|
||||
@@ -319,9 +321,7 @@ class MailPart {
|
||||
CountingOutputStream cos = null;
|
||||
Buffer dout = null;
|
||||
try {
|
||||
in = buffer.getInputStream();
|
||||
DataHelper.skip(in, buffer.getOffset() + beginBody + offset);
|
||||
lin = new LimitInputStream(in, end - beginBody - offset);
|
||||
lin = getRawInputStream(offset);
|
||||
if (decodedLength < 0) {
|
||||
cos = new CountingOutputStream(out.getOutputStream());
|
||||
dout = new OutputStreamBuffer(cos);
|
||||
@@ -341,7 +341,6 @@ class MailPart {
|
||||
Debug.debug(Debug.DEBUG, "Decode IOE", ioe);
|
||||
throw ioe;
|
||||
} finally {
|
||||
if (in != null) try { in.close(); } catch (IOException ioe) {};
|
||||
if (lin != null) try { lin.close(); } catch (IOException ioe) {};
|
||||
buffer.readComplete(true);
|
||||
// let the servlet do this
|
||||
@@ -354,6 +353,39 @@ class MailPart {
|
||||
decodedLength = (int) cos.getWritten();
|
||||
}
|
||||
|
||||
/**
|
||||
* Synched because FileBuffer keeps stream open
|
||||
* Caller must close out
|
||||
*
|
||||
* @since 0.9.35
|
||||
*/
|
||||
public synchronized void outputRaw(OutputStream out) throws IOException {
|
||||
LimitInputStream lin = null;
|
||||
try {
|
||||
lin = getRawInputStream(0);
|
||||
DataHelper.copy(lin, out);
|
||||
} catch (IOException ioe) {
|
||||
Debug.debug(Debug.DEBUG, "Decode IOE", ioe);
|
||||
throw ioe;
|
||||
} finally {
|
||||
if (lin != null) try { lin.close(); } catch (IOException ioe) {};
|
||||
buffer.readComplete(true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Synched because FileBuffer keeps stream open
|
||||
* Caller must call readComplete() on buffer
|
||||
*
|
||||
* @param offset 2 for sendAttachment, 0 otherwise, probably for \r\n
|
||||
* @since 0.9.35
|
||||
*/
|
||||
private synchronized LimitInputStream getRawInputStream(int offset) throws IOException {
|
||||
InputStream in = buffer.getInputStream();
|
||||
DataHelper.skip(in, buffer.getOffset() + beginBody + offset);
|
||||
return new LimitInputStream(in, end - beginBody - offset);
|
||||
}
|
||||
|
||||
private static String getFirstAttribute( String line )
|
||||
{
|
||||
String result = null;
|
||||
|
||||
@@ -52,7 +52,9 @@ import net.i2p.util.SystemVersion;
|
||||
* 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.
|
||||
* This class should only be accessed from MailCache.
|
||||
*
|
||||
* TODO cached server caps and config.
|
||||
*
|
||||
* @since 0.9.14
|
||||
*/
|
||||
@@ -68,17 +70,16 @@ class PersistentMailCache {
|
||||
|
||||
private final Object _lock;
|
||||
private final File _cacheDir;
|
||||
// non-null only for Drafts
|
||||
private final File _attachmentDir;
|
||||
private final I2PAppContext _context;
|
||||
private final boolean _isDrafts;
|
||||
|
||||
private static final String DIR_SUSI = "susimail";
|
||||
private static final String DIR_CACHE = "cache";
|
||||
private static final String CACHE_PREFIX = "cache-";
|
||||
public static final String DIR_FOLDER = "cur"; // MailDir-like
|
||||
public static final String DIR_DRAFTS = "Drafts"; // MailDir-like
|
||||
public static final String DIR_SENT = "Sent"; // MailDir-like
|
||||
public static final String DIR_TRASH = "Trash"; // MailDir-like
|
||||
public static final String DIR_SPAM = "Bulk Mail"; // MailDir-like
|
||||
public static final String DIR_IMPORT = "import"; // Flat with .eml files, debug only for now
|
||||
public static final String DIR_ATTACHMENTS = "attachments"; // Flat with draft attachment files
|
||||
private static final String DIR_PREFIX = "s";
|
||||
private static final String FILE_PREFIX = "mail-";
|
||||
private static final String HDR_SUFFIX = ".hdr.txt.gz";
|
||||
@@ -91,16 +92,23 @@ class PersistentMailCache {
|
||||
* Does NOT load the mails in. Caller MUST call getMails().
|
||||
*
|
||||
* @param pass ignored
|
||||
* @param folder use DIR_FOLDER
|
||||
* @param folder e.g. DIR_FOLDER
|
||||
*/
|
||||
public PersistentMailCache(I2PAppContext ctx, String host, int port, String user, String pass, String folder) throws IOException {
|
||||
_context = ctx;
|
||||
_isDrafts = folder.equals(WebMail.DIR_DRAFTS);
|
||||
_lock = getLock(host, port, user, pass);
|
||||
synchronized(_lock) {
|
||||
_cacheDir = makeCacheDirs(host, port, user, pass, folder);
|
||||
// Debugging only for now.
|
||||
if (folder.equals(DIR_FOLDER))
|
||||
File attach = null;
|
||||
if (folder.equals(WebMail.DIR_FOLDER)) {
|
||||
importMail();
|
||||
} else if (folder.equals(WebMail.DIR_DRAFTS)) {
|
||||
attach = new SecureDirectory(_cacheDir, DIR_ATTACHMENTS);
|
||||
attach.mkdirs();
|
||||
}
|
||||
_attachmentDir = attach;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -143,7 +151,7 @@ class PersistentMailCache {
|
||||
int tcnt = Math.max(1, Math.min(sz / 4, Math.min(SystemVersion.getCores(), 16)));
|
||||
List<Thread> threads = new ArrayList<Thread>(tcnt);
|
||||
for (int i = 0; i < tcnt; i++) {
|
||||
Thread t = new I2PAppThread(new Loader(fq, rv), "Email loader " + i);
|
||||
Thread t = new I2PAppThread(new Loader(fq, rv, _isDrafts), "Email loader " + i);
|
||||
t.start();
|
||||
threads.add(t);
|
||||
}
|
||||
@@ -166,15 +174,17 @@ class PersistentMailCache {
|
||||
private static class Loader implements Runnable {
|
||||
private final Queue<File> _in;
|
||||
private final Queue<Mail> _out;
|
||||
private final boolean _isD;
|
||||
|
||||
public Loader(Queue<File> in, Queue<Mail> out) {
|
||||
public Loader(Queue<File> in, Queue<Mail> out, boolean isDrafts) {
|
||||
_in = in; _out = out;
|
||||
_isD = isDrafts;
|
||||
}
|
||||
|
||||
public void run() {
|
||||
File f;
|
||||
while ((f = _in.poll()) != null) {
|
||||
Mail mail = load(f);
|
||||
Mail mail = load(f, _isD);
|
||||
if (mail != null)
|
||||
_out.offer(mail);
|
||||
}
|
||||
@@ -296,14 +306,33 @@ class PersistentMailCache {
|
||||
return base;
|
||||
}
|
||||
|
||||
private File getHeaderFile(String uidl) {
|
||||
public File getHeaderFile(String uidl) {
|
||||
return getFile(uidl, HDR_SUFFIX);
|
||||
}
|
||||
|
||||
private File getFullFile(String uidl) {
|
||||
public File getFullFile(String uidl) {
|
||||
return getFile(uidl, FULL_SUFFIX);
|
||||
}
|
||||
|
||||
/**
|
||||
* For reading or writing a new full mail (NOT headers only).
|
||||
* For writing, caller MUST call writeComplete() on rv.
|
||||
* Does not necessarily exist.
|
||||
*
|
||||
* @since 0.9.35
|
||||
*/
|
||||
public GzipFileBuffer getFullBuffer(String uidl) {
|
||||
return new GzipFileBuffer(getFile(uidl, FULL_SUFFIX));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return non-null only for Drafts
|
||||
* @since 0.9.35
|
||||
*/
|
||||
public File getAttachmentDir() {
|
||||
return _attachmentDir;
|
||||
}
|
||||
|
||||
private File getFile(String uidl, String suffix) {
|
||||
byte[] raw = DataHelper.getASCII(uidl);
|
||||
byte[] md5 = PasswordManager.md5Sum(raw);
|
||||
@@ -351,7 +380,7 @@ class PersistentMailCache {
|
||||
*
|
||||
* @return null on failure
|
||||
*/
|
||||
private static Mail load(File f) {
|
||||
private static Mail load(File f, boolean isDrafts) {
|
||||
String name = f.getName();
|
||||
String uidl;
|
||||
boolean headerOnly;
|
||||
@@ -369,7 +398,11 @@ class PersistentMailCache {
|
||||
Buffer rb = read(f);
|
||||
if (rb == null)
|
||||
return null;
|
||||
Mail mail = new Mail(uidl);
|
||||
Mail mail;
|
||||
if (isDrafts)
|
||||
mail = new Draft(uidl);
|
||||
else
|
||||
mail = new Mail(uidl);
|
||||
if (headerOnly)
|
||||
mail.setHeader(rb);
|
||||
else
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -131,7 +131,7 @@ public abstract class Encoding {
|
||||
|
||||
/**
|
||||
* This implementation just converts the string to a byte array
|
||||
* and then calls encode(byte[]).
|
||||
* and then calls decode(byte[]).
|
||||
* Most classes will not need to override.
|
||||
*
|
||||
* @param str
|
||||
|
||||
@@ -134,14 +134,14 @@ public class HeaderLine extends Encoding {
|
||||
* TODO this will not work for quoting structured text
|
||||
* such as recipient names on the "To" and "Cc" lines.
|
||||
*
|
||||
* @param str must start with "field-name: "
|
||||
* @param str must start with "field-name: ", must have non-whitespace after that
|
||||
*/
|
||||
@Override
|
||||
public String encode(String str) throws EncodingException {
|
||||
str = str.trim();
|
||||
int l = str.indexOf(": ");
|
||||
if (l <= 0 || l >= 64)
|
||||
throw new EncodingException("bad 'field-name: '" + str);
|
||||
throw new EncodingException("bad field-name: " + str);
|
||||
l += 2;
|
||||
boolean quote = false;
|
||||
if (str.length() > 76) {
|
||||
|
||||
@@ -221,7 +221,7 @@ public class SMTPClient {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param body without the attachments
|
||||
* @param body headers and body, without the attachments
|
||||
* @param attachments may be null
|
||||
* @param boundary non-null if attachments is non-null
|
||||
* @return success
|
||||
@@ -248,6 +248,7 @@ public class SMTPClient {
|
||||
socket.setSoTimeout(120*1000);
|
||||
int result = sendCmd(null);
|
||||
if (result != 220) {
|
||||
error += _t("Error sending mail") + '\n';
|
||||
if (result != 0)
|
||||
error += _t("Server refused connection") + " (" + result + ")\n";
|
||||
else
|
||||
@@ -331,39 +332,7 @@ public class SMTPClient {
|
||||
//socket.getOutputStream().write(DataHelper.getASCII("\r\n.\r\n"));
|
||||
// Do it this way so we don't double the memory
|
||||
out = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream(), "ISO-8859-1"));
|
||||
out.write(body.toString());
|
||||
// moved from WebMail so we don't bring the attachments into memory
|
||||
// Also TODO use the 250 service extension responses to pick the best encoding
|
||||
// and check the max total size
|
||||
if (attachments != null && !attachments.isEmpty()) {
|
||||
for(Attachment attachment : attachments) {
|
||||
String encodeTo = attachment.getTransferEncoding();
|
||||
Encoding encoding = EncodingFactory.getEncoding(encodeTo);
|
||||
if (encoding == null)
|
||||
throw new EncodingException( _t("No Encoding found for {0}", encodeTo));
|
||||
// ref: https://blog.nodemailer.com/2017/01/27/the-mess-that-is-attachment-filenames/
|
||||
// ref: RFC 2231
|
||||
// split Content-Disposition into 3 lines to maximize room
|
||||
// TODO filename*0* for long names...
|
||||
String name = attachment.getFileName();
|
||||
String name2 = FilenameUtil.sanitizeFilename(name);
|
||||
String name3 = FilenameUtil.encodeFilenameRFC5987(name);
|
||||
out.write("\r\n--" + boundary +
|
||||
"\r\nContent-type: " + attachment.getContentType() +
|
||||
"\r\nContent-Disposition: attachment;\r\n\tfilename=\"" + name2 +
|
||||
"\";\r\n\tfilename*=" + name3 +
|
||||
"\r\nContent-Transfer-Encoding: " + attachment.getTransferEncoding() +
|
||||
"\r\n\r\n");
|
||||
InputStream in = null;
|
||||
try {
|
||||
in = attachment.getData();
|
||||
encoding.encode(in, out);
|
||||
} finally {
|
||||
if (in != null) try { in.close(); } catch (IOException ioe) {}
|
||||
}
|
||||
}
|
||||
out.write( "\r\n--" + boundary + "--\r\n" );
|
||||
}
|
||||
writeMail(out, body, attachments, boundary);
|
||||
out.write("\r\n.\r\n");
|
||||
out.flush();
|
||||
socket.setSoTimeout(0);
|
||||
@@ -391,6 +360,50 @@ public class SMTPClient {
|
||||
return mailSent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Caller must close out
|
||||
*
|
||||
* @param body headers and body, without the attachments
|
||||
* @param attachments may be null
|
||||
* @param boundary non-null if attachments is non-null
|
||||
*/
|
||||
public static void writeMail(Writer out, StringBuilder body,
|
||||
List<Attachment> attachments, String boundary) throws IOException {
|
||||
out.write(body.toString());
|
||||
// moved from WebMail so we don't bring the attachments into memory
|
||||
// Also TODO use the 250 service extension responses to pick the best encoding
|
||||
// and check the max total size
|
||||
if (attachments != null && !attachments.isEmpty()) {
|
||||
for(Attachment attachment : attachments) {
|
||||
String encodeTo = attachment.getTransferEncoding();
|
||||
Encoding encoding = EncodingFactory.getEncoding(encodeTo);
|
||||
if (encoding == null)
|
||||
throw new EncodingException( _t("No Encoding found for {0}", encodeTo));
|
||||
// ref: https://blog.nodemailer.com/2017/01/27/the-mess-that-is-attachment-filenames/
|
||||
// ref: RFC 2231
|
||||
// split Content-Disposition into 3 lines to maximize room
|
||||
// TODO filename*0* for long names...
|
||||
String name = attachment.getFileName();
|
||||
String name2 = FilenameUtil.sanitizeFilename(name);
|
||||
String name3 = FilenameUtil.encodeFilenameRFC5987(name);
|
||||
out.write("\r\n--" + boundary +
|
||||
"\r\nContent-type: " + attachment.getContentType() +
|
||||
"\r\nContent-Disposition: attachment;\r\n\tfilename=\"" + name2 +
|
||||
"\";\r\n\tfilename*=" + name3 +
|
||||
"\r\nContent-Transfer-Encoding: " + attachment.getTransferEncoding() +
|
||||
"\r\n\r\n");
|
||||
InputStream in = null;
|
||||
try {
|
||||
in = attachment.getData();
|
||||
encoding.encode(in, out);
|
||||
} finally {
|
||||
if (in != null) try { in.close(); } catch (IOException ioe) {}
|
||||
}
|
||||
}
|
||||
out.write( "\r\n--" + boundary + "--\r\n" );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A command to send and a result code to expect
|
||||
* @since 0.9.13
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
2018-04-14 zzz
|
||||
* Console: Add built-by to /logs (ticket #2204)
|
||||
* CPUID: Fix TBM detection (ticket #2211)
|
||||
* Debian updates (ticket #2027, PR #15)
|
||||
* Jetty: Fix quote in header line tripping XSS filter (ticket #2215)
|
||||
* SusiMail: Add folders, drafts, background sending (ticket #2087)
|
||||
|
||||
2018-04-11 zzz
|
||||
* Debian updates for 0.9.34
|
||||
* Jetty 9.2.24-v201801015
|
||||
* Tomcat 8.5.30
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ public class RouterVersion {
|
||||
/** deprecated */
|
||||
public final static String ID = "Monotone";
|
||||
public final static String VERSION = CoreVersion.VERSION;
|
||||
public final static long BUILD = 1;
|
||||
public final static long BUILD = 2;
|
||||
|
||||
/** for example "-test" */
|
||||
public final static String EXTRA = "";
|
||||
|
||||
Reference in New Issue
Block a user