");
+ log.debug("after removeBeforeBody1");
+
+ html = Patterns.removeBeforeBody2.matcher(html).replaceFirst("
");
+ log.debug("after removeAfterBody");
+
+ // remove the html tag ender and everyhting aftewards
+ html = Patterns.removeEndHtml.matcher(html).replaceAll("");
+ log.debug("after removeEndHtml");
+
+ return html;
+ }
+ */
+
+ /* maybe for another day
+ public String stripHTML (String html) throws Exception
+ {
+ DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
+ dbf.setValidating(false);
+ dbf.setNamespaceAware(true);
+ dbf.setIgnoringComments(false);
+ dbf.setIgnoringElementContentWhitespace(false);
+ dbf.setExpandEntityReferences(false);
+ DocumentBuilder db = dbf.newDocumentBuilder();
+ Document doc = db.parse(new StringInputStream(html));
+ }
+ */
+
+ protected static Pair
findTag(boolean last, String html, String tag, Pair search)
+ {
+ // adkjfkasdflk asdlkasd < html > ojwefijoweiofjoiwjfwe
+ // find: "html"
+
+ Pair range = Pair.create(search.first, search.second);
+ int pos = -1;
+
+ while (true)
+ {
+
+ if (last)
+ {
+ if (pos != -1)
+ range.second = pos-1;
+
+ pos = html.lastIndexOf(tag, range.second);
+ if (pos == -1 || pos < range.first)
+ return null;
+ }
+ else
+ {
+ if (pos != -1)
+ range.first = pos + tag.length();
+
+ if (html.length() <= range.first + tag.length())
+ return null;
+
+ pos = html.indexOf(tag, range.first);
+ if (pos == -1 || pos > range.second)
+ return null;
+ }
+
+ // search backwards for <
+ boolean restart = false;
+ int i;
+ for (i=pos-1; i>=0; i--)
+ {
+ char c = html.charAt(i);
+ if (c=='<')
+ break;
+ else
+ // wan't our tag signal for restart search
+ if (!Characters.isWhitespace(c))
+ {
+ restart = true;
+ break;
+ }
+ }
+
+ // restart the search if signaled
+ if (restart)
+ continue;
+
+ if (i == -1)
+ return null;
+
+ int j;
+ for (j=pos+tag.length(); j')
+ break;
+ }
+
+ // restart the search
+ if (j == html.length())
+ continue;
+
+ log.debug("found",tag,i,j);
+
+ return Pair.create(i,j+1);
+ }
+ }
+
+ protected static void andLeftRange (Pair range, Pair tag)
+ {
+ if (tag == null)
+ return;
+
+ if (tag.second > range.first)
+ range.first = tag.second;
+ }
+
+ protected static void andRightRange (Pair range, Pair tag)
+ {
+ if (tag == null)
+ return;
+
+ if (tag.first < range.second)
+ range.second = tag.first;
+ }
+
+ public static String stripHTML (String html)
+ {
+ if (html == null)
+ return null;
+
+ String lower = html.toLowerCase();
+ Pair range = Pair.create(0, html.length());
+ Pair tag;
+
+ //-----------------
+
+ tag = findTag(true, lower, "body", range);
+ andLeftRange(range,tag);
+
+ tag = findTag(true, lower, "html", range);
+ andLeftRange(range,tag);
+
+ tag = findTag(true, lower, "/head", range);
+ andLeftRange(range,tag);
+
+ //----------------
+
+ tag = findTag(false, lower, "/body", range);
+ andRightRange(range,tag);
+
+ tag = findTag(false, lower, "/html", range);
+ andRightRange(range,tag);
+
+ //----------------
+ String result = html.substring(range.first, range.second);
+ log.debug("stripHTML", result);
+ return result;
+ }
+
+ /*
+ public static void main (String[] args)
+ {
+ String html = "kjhasdfkjakjd < html > < body > woeifjweijf ";
+ System.out.println(stripHTML(html));
+ }
+ */
+
+ public void setHTML(String html)
+ {
+ this.html = html;
+ }
+
+ public String calculateBrief ()
+ {
+ if (text == null)
+ return null;
+
+ String content = calculateTextWithoutReply();
+ int length = Math.min(content.length(), 256);
+ String brief = content.substring(0, length).replace("\n", " ");
+
+ return brief;
+ }
+
+ public String calculateReply ()
+ {
+ if (text == null)
+ return "";
+
+ try
+ {
+ ArrayList lines = new ArrayList();
+ BufferedReader r = new BufferedReader(new StringReader(text));
+ String line;
+ while ((line = r.readLine()) != null)
+ {
+ lines.add("> " + line);
+ }
+
+ return Strings.concat(lines.iterator(), "\n").trim();
+ }
+ catch (Exception e)
+ {
+ return "Failed to calculate reply";
+ }
+ }
+
+ public boolean isProbablyReplyHeader (String s)
+ {
+ return (s.endsWith(":") && s.contains("On"));
+ }
+
+ public boolean isPossiblyQuoteBeginning (String s)
+ {
+ return s.startsWith("--") || s.startsWith("==");
+ }
+
+ public String calculateTextWithoutReply ()
+ {
+ if (text == null)
+ return "";
+
+ try
+ {
+ ArrayList lines = new ArrayList();
+ BufferedReader r = new BufferedReader(new StringReader(text));
+ String line;
+
+ boolean alreadyFoundReply = false;
+ int possiblyFoundQuote = -1;
+ while ((line = r.readLine()) != null)
+ {
+ if (line.startsWith(">"))
+ {
+ if (!alreadyFoundReply)
+ {
+ alreadyFoundReply = true;
+
+ for (int i=0; i<5; ++i)
+ {
+ int index = (lines.size()-i)-1;
+ if (index < 0)
+ break;
+
+ if (isProbablyReplyHeader(lines.get(index)))
+ {
+ assert(index >=0 );
+ while (lines.size() > index)
+ lines.remove(lines.size()-1);
+
+ break;
+ }
+ }
+ }
+ }
+ else
+ {
+ lines.add(line);
+
+ if (isPossiblyQuoteBeginning(line))
+ possiblyFoundQuote = lines.size()-1;
+ }
+ }
+
+ while (!lines.isEmpty() && lines.get(lines.size()-1).trim().isEmpty())
+ lines.remove(lines.size()-1);
+
+ if (possiblyFoundQuote >= 0 && possiblyFoundQuote > (lines.size()-6))
+ {
+ while (lines.size() > possiblyFoundQuote)
+ lines.remove(lines.size()-1);
+ }
+
+ return Strings.concat(lines.iterator(), "\n").trim();
+ }
+ catch (Exception e)
+ {
+ return e.toString();
+ }
+ }
+}
diff --git a/java/core/src/core/mail/client/model/ConstantsMisc.java b/java/core/src/core/mail/client/model/ConstantsMisc.java
new file mode 100644
index 0000000..6e41d47
--- /dev/null
+++ b/java/core/src/core/mail/client/model/ConstantsMisc.java
@@ -0,0 +1,12 @@
+/**
+ * Author: Timothy Prepscius
+ * License: GPLv3 Affero + keep my name in the code!
+ */
+package mail.client.model;
+
+public class ConstantsMisc {
+
+ public static String ALL = "All";
+ public static final String REPLY_PREFIX = "Re:";
+
+}
diff --git a/java/core/src/core/mail/client/model/Conversation.java b/java/core/src/core/mail/client/model/Conversation.java
new file mode 100644
index 0000000..e372209
--- /dev/null
+++ b/java/core/src/core/mail/client/model/Conversation.java
@@ -0,0 +1,190 @@
+/**
+ * Author: Timothy Prepscius
+ * License: GPLv3 Affero + keep my name in the code!
+ */
+package mail.client.model;
+
+import org.timepedia.exporter.client.Export;
+import org.timepedia.exporter.client.Exportable;
+import org.timepedia.exporter.client.NoExport;
+
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import core.util.Collectionz;
+import core.util.Comparators;
+import core.util.LogNull;
+import core.util.Pair;
+import mail.client.CacheManager;
+import mail.client.Events;
+import mail.client.cache.ID;
+
+@Export
+public class Conversation extends Model implements Exportable
+{
+ static LogNull log = new LogNull(Conversation.class);
+
+ static class SortByDateLatestFirst implements Comparator {
+
+ @Override
+ public int compare(Conversation l, Conversation r)
+ {
+ Date ld = l.getHeader().getDate(), rd = r.getHeader().getDate();
+ return rd.compareTo(ld);
+ }
+ }
+
+ protected Header header;
+ protected List> items;
+ protected Set itemIds = new HashSet();
+
+ public Conversation (CacheManager manager)
+ {
+ super(manager);
+ reset();
+
+ getLoadCallbacks()
+ .addCallback(manager.getMaster().getEventPropagator().signal_(Events.LoadConversation, this));
+ }
+
+ public void reset ()
+ {
+ items = new ArrayList>();
+ recomputeHeader();
+ }
+
+ protected void recomputeHeader ()
+ {
+ header = new Header();
+ header.setDictionary(new Dictionary());
+ header.setAuthors(new ArrayList());
+ header.setRecipients(new Recipients());
+ header.setTransportState(TransportState.NONE());
+
+ for (Pair p : items)
+ {
+ Mail m = getManager().getMail(p.first);
+ if (m.isLoaded())
+ accumulate(m);
+ }
+ }
+
+ protected void accumulate(Mail m)
+ {
+ Header h = m.getHeader();
+
+ if (h.getAuthor() != null)
+ header.getAuthors().add(h.getAuthor());
+
+ if (header.getDate() == null || h.getDate().after(header.getDate()))
+ {
+ header.setDate(h.getDate());
+ header.setBrief(h.getBrief());
+ header.setSubject(h.getSubjectExcludingReplyPrefix());
+ }
+
+ if (h.getRecipients() != null)
+ header.getRecipients().add(h.getRecipients());
+
+ header.getDictionary().add(m);
+ header.getTransportState().mark(h.getTransportState());
+ header.unmarkState(TransportState.READ);
+ }
+
+ public List getItems ()
+ {
+ List result = new ArrayList(items.size());
+ for (Pair p : items)
+ {
+ Mail m = getManager().getMail(p.first);
+ result.add(m);
+ }
+
+ return result;
+ }
+
+ public List> getItemIds ()
+ {
+ return items;
+ }
+
+ public void addItemId (ID id, Date date)
+ {
+ items.add(new Pair(id,date));
+ Collections.sort(items, new Comparators.SortBySecondNatural());
+ }
+
+ public void removeItemId (ID id)
+ {
+ Collectionz.removeByFirst(items, id);
+ }
+
+ public void addItem (Mail mail)
+ {
+ addItemId (mail.getId(), mail.getHeader().getDate());
+ accumulate (mail);
+
+ markDirty();
+ }
+
+ public void removeItem (Mail mail)
+ {
+ removeItemId (mail.getId());
+ recomputeHeader();
+
+ markDirty();
+ }
+
+ public void itemChanged (Mail mail)
+ {
+ for (Pair p : items)
+ {
+ if (p.first == mail.getId())
+ p.second = mail.getHeader().getDate();
+ }
+
+ Collections.sort(items, new Comparators.SortBySecondNatural());
+ recomputeHeader();
+
+ markDirty();
+ }
+
+ public Header getHeader ()
+ {
+ return header;
+ }
+
+ public void setHeader (Header header)
+ {
+ this.header = header;
+ }
+
+ public int getNumItems ()
+ {
+ return items.size();
+ }
+
+ public void markState (String state)
+ {
+ if (!getHeader().hasState(state))
+ {
+ getHeader().markState(state);
+ markDirty();
+ }
+ }
+
+ public void unmarkState (String state)
+ {
+ if (getHeader().hasState(state))
+ {
+ getHeader().unmarkState(state);
+ markDirty();
+ }
+ }
+}
diff --git a/java/core/src/core/mail/client/model/Dictionary.java b/java/core/src/core/mail/client/model/Dictionary.java
new file mode 100644
index 0000000..89d7f28
--- /dev/null
+++ b/java/core/src/core/mail/client/model/Dictionary.java
@@ -0,0 +1,282 @@
+/**
+ * Author: Timothy Prepscius
+ * License: GPLv3 Affero + keep my name in the code!
+ */
+package mail.client.model;
+
+import java.io.Serializable;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.StringTokenizer;
+
+import core.util.Comparators;
+import core.util.FastRandom;
+import core.util.LogNull;
+import core.util.LogOut;
+import core.util.Pair;
+import core.util.Strings;
+
+public class Dictionary implements Serializable
+{
+ private static final long serialVersionUID = 1L;
+ static LogNull log = new LogNull(Dictionary.class);
+ static FastRandom random = new FastRandom();
+
+ public Map vocabulary;
+ int bayesianSize=0;
+
+ public Dictionary ()
+ {
+ vocabulary = new HashMap();
+ }
+
+ public Dictionary (String filter)
+ {
+ this();
+
+ add(filter);
+ }
+
+ public Dictionary (Mail mail)
+ {
+ this();
+
+ add (mail);
+ }
+
+ public Map getVocabulary ()
+ {
+ return vocabulary;
+ }
+
+ final String TOKENS = " \t\r\n!@#$%^&*()_+-=`~{}[]\\|;:'\",./<>?";
+
+ public Dictionary add(String text)
+ {
+ if (text != null)
+ {
+ StringTokenizer st = new StringTokenizer(text,TOKENS);
+ while (st.hasMoreTokens())
+ {
+ String token = st.nextToken().toLowerCase();
+
+ int occurences = 0;
+
+ if (vocabulary.containsKey(token))
+ occurences = vocabulary.get(token);
+
+ bayesianSize ++;
+ vocabulary.put(token, new Integer(occurences+1));
+ }
+ }
+
+ return this;
+ }
+
+ public Dictionary add (Mail mail)
+ {
+ if (mail.getHeader().getAuthor()!=null)
+ add (mail.getHeader().getAuthor().toString());
+
+ if (mail.getHeader().getRecipients()!=null)
+ for (Identity i : mail.getHeader().getRecipients().getAll())
+ add(i.toString());
+
+ add (mail.getBody().getText());
+ add (mail.getHeader().getSubject());
+
+ log.debug(this, "after add", toSerializableString());
+
+ return this;
+ }
+
+ public boolean matches (Dictionary filter)
+ {
+ final String Q = "\"";
+
+ for (Entry i : filter.vocabulary.entrySet())
+ {
+ String match = i.getKey();
+
+ boolean exact = (match.startsWith(Q) && match.endsWith(Q));
+
+ if (exact)
+ {
+ match = match.substring(1, match.length()-1);
+ log.debug("match is surrounded by quotes, using exact match:",match);
+ }
+
+ if (!vocabulary.containsKey(match))
+ {
+ if (exact)
+ return false;
+
+ boolean found = false;
+ for (Entry j : vocabulary.entrySet())
+ {
+ if (j.getKey().startsWith(match))
+ {
+ found = true;
+ break;
+ }
+ }
+ if (!found)
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ public String toSerializableString()
+ {
+ String[] strings = new String[vocabulary.size()];
+
+ int j=0;
+ for (Entry i : vocabulary.entrySet())
+ {
+ strings[j++] = i.getKey() + ":" + i.getValue();
+ }
+
+ return Strings.concat(strings, ",");
+ }
+
+ public void fromSerializableString(String string)
+ {
+ String[] strings = string.split(",");
+
+ for (String i : strings)
+ {
+ if (i.isEmpty())
+ continue;
+
+ try
+ {
+ String[] split = i.split(":");
+ int occurences = Integer.parseInt(split[1]);
+
+ bayesianSize += occurences;
+ vocabulary.put(split[0], occurences);
+ }
+ catch (Exception e)
+ {
+ log.exception(e);
+ continue;
+ }
+ }
+ }
+
+ public void add (Dictionary dictionary)
+ {
+ for (Entry i : dictionary.vocabulary.entrySet())
+ {
+ int occurences = 0;
+ String token = i.getKey();
+
+ if (vocabulary.containsKey(i))
+ occurences = vocabulary.get(token);
+
+ bayesianSize += i.getValue();
+ vocabulary.put(token, new Integer(occurences+i.getValue()));
+ }
+
+ bayesianPrune();
+ }
+
+ public void subtract(Dictionary dictionary)
+ {
+ for (Entry i : dictionary.vocabulary.entrySet())
+ {
+ int occurences = 0;
+ String token = i.getKey();
+
+ if (vocabulary.containsKey(i))
+ occurences = vocabulary.get(token);
+
+ bayesianSize -= i.getValue();
+ vocabulary.put(token, new Integer(occurences-i.getValue()));
+ }
+
+ bayesianPrune();
+ }
+
+ protected float bayesianProbabilityOfTerm (String term)
+ {
+ Integer v = vocabulary.get(term);
+ if (v == null)
+ return 0.0f;
+
+ log.trace(this, "bayesianProbabilityOfTerm", term, v, "+1 /", bayesianSize);
+
+ return (float)(v + 1)/(float)bayesianSize;
+ }
+
+ public float bayesianProbability (Dictionary match)
+ {
+ float probability = 0.0f;
+ for (Entry i : match.vocabulary.entrySet())
+ {
+// probability += (float)i.getValue() * bayesianProbabilityOfTerm(i.getKey());
+ probability += bayesianProbabilityOfTerm(i.getKey());
+ }
+
+ return probability;
+ }
+
+ void bayesianPrune ()
+ {
+ List> remove = new ArrayList>();
+
+ // remove all negative and zero values
+ for (Entry i : vocabulary.entrySet())
+ {
+ if (i.getValue() <= 0)
+ remove.add(new Pair(i.getKey(), i.getValue()));
+ }
+
+ for (Pair i : remove)
+ {
+ bayesianSize -= i.second;
+ vocabulary.remove(i.first);
+ }
+
+ remove.clear();
+
+ // remove some parts of the remaining
+ for (Entry i : vocabulary.entrySet())
+ remove.add(new Pair(i.getKey(), i.getValue()));
+
+ Collections.sort(remove, new Comparators.SortBySecondNatural());
+
+ float numToPossiblyRemove = remove.size() - 100;
+ float numOkAfterThreshold = 100;
+ if (numToPossiblyRemove < numOkAfterThreshold)
+ return;
+
+ float probabilityOfRemoval = numToPossiblyRemove/(numToPossiblyRemove + numOkAfterThreshold);
+ log.trace(this, "bayesianPrune", probabilityOfRemoval, numToPossiblyRemove, numOkAfterThreshold);
+
+ for (int i=0; i term = remove.get(i);
+ bayesianSize -= term.second;
+ vocabulary.remove(term.first);
+ }
+ }
+ }
+
+ public boolean bayesianMatches(Dictionary dictionary)
+ {
+ float result = bayesianProbability(dictionary);
+ log.debug(this, "bayesianProbability", result, this.toSerializableString(), dictionary.toSerializableString());
+
+ return result > 0.5f;
+ }
+
+}
diff --git a/java/core/src/core/mail/client/model/Direction.java b/java/core/src/core/mail/client/model/Direction.java
new file mode 100644
index 0000000..8eb1dcd
--- /dev/null
+++ b/java/core/src/core/mail/client/model/Direction.java
@@ -0,0 +1,11 @@
+/**
+ * Author: Timothy Prepscius
+ * License: GPLv3 Affero + keep my name in the code!
+ */
+package mail.client.model;
+
+public enum Direction
+{
+ IN,
+ OUT
+};
\ No newline at end of file
diff --git a/java/core/src/core/mail/client/model/Folder.java b/java/core/src/core/mail/client/model/Folder.java
new file mode 100644
index 0000000..0704d47
--- /dev/null
+++ b/java/core/src/core/mail/client/model/Folder.java
@@ -0,0 +1,66 @@
+/**
+ * Author: Timothy Prepscius
+ * License: GPLv3 Affero + keep my name in the code!
+ */
+package mail.client.model;
+
+import java.util.Date;
+import java.util.List;
+
+import core.callback.Callback;
+import core.util.Pair;
+import mail.client.CacheManager;
+import mail.client.cache.ID;
+
+import org.timepedia.exporter.client.Export;
+import org.timepedia.exporter.client.Exportable;
+import org.timepedia.exporter.client.NoExport;
+
+@Export()
+public abstract class Folder extends Model implements Exportable
+{
+ FolderDefinition folderDefinition;
+
+ @NoExport()
+ public Folder (CacheManager manager)
+ {
+ super(manager);
+ }
+
+ public FolderDefinition getFolderDefinition()
+ {
+ return folderDefinition;
+ }
+
+ public void setFolderDefinition(FolderDefinition folderDefinition)
+ {
+ this.folderDefinition = folderDefinition;
+ }
+
+ public String getName()
+ {
+ return folderDefinition.getName();
+ }
+
+ public void setName(String name)
+ {
+ folderDefinition.setName(name);
+ markDirty();
+ }
+
+ public abstract List> getConversationIds ();
+ public abstract void addConversationId (ID id, Date date);
+ public abstract boolean isFull ();
+
+ public abstract List getConversations (int from, int length, String filter);
+ public abstract boolean hasConversation (Conversation conversation);
+ public abstract void conversationAdded (Conversation conversation);
+ public abstract void conversationDeleted (Conversation conversation);
+ public abstract Conversation getMatchingConversation (Header header);
+
+ public final void conversationChanged (Conversation conversation)
+ {
+ conversationDeleted(conversation);
+ conversationAdded(conversation);
+ }
+}
diff --git a/java/core/src/core/mail/client/model/FolderDefinition.java b/java/core/src/core/mail/client/model/FolderDefinition.java
new file mode 100644
index 0000000..6a80611
--- /dev/null
+++ b/java/core/src/core/mail/client/model/FolderDefinition.java
@@ -0,0 +1,167 @@
+/**
+ * Author: Timothy Prepscius
+ * License: GPLv3 Affero + keep my name in the code!
+ */
+package mail.client.model;
+
+import org.timepedia.exporter.client.Export;
+import org.timepedia.exporter.client.Exportable;
+
+import core.util.LogNull;
+import core.util.LogNull;
+
+@Export
+public class FolderDefinition implements Exportable
+{
+ static LogNull log = new LogNull(FolderDefinition.class);
+
+ String name;
+ Identity author;
+ Identity recipient;
+ String subject;
+ TransportState stateEquals, stateDiffers;
+
+ boolean autoBayesian = false;
+ Dictionary bayesianDictionary;
+
+ public FolderDefinition (String name)
+ {
+ this.name = name;
+ }
+
+ public String getName ()
+ {
+ return name;
+ }
+
+ public void setName(String name)
+ {
+ this.name = name;
+ }
+
+ public FolderDefinition setAuthor(Identity author)
+ {
+ this.author = author;
+ return this;
+ }
+
+ public Identity getAuthor ()
+ {
+ return author;
+ }
+
+ public FolderDefinition setRecipient(Identity recipient)
+ {
+ this.recipient = recipient;
+ return this;
+ }
+
+ public Identity getRecipient ()
+ {
+ return recipient;
+ }
+
+ public FolderDefinition setSubject(String subject)
+ {
+ this.subject = subject;
+ return this;
+ }
+
+ public String getSubject ()
+ {
+ return subject;
+ }
+
+ public FolderDefinition setState(TransportState stateEquals, TransportState stateDiffers)
+ {
+ this.stateEquals = stateEquals;
+ this.stateDiffers = stateDiffers;
+ return this;
+ }
+
+ public TransportState getStateEquals ()
+ {
+ return stateEquals;
+ }
+
+ public TransportState getStateDiffers ()
+ {
+ return stateDiffers;
+ }
+
+ public boolean matchesFilter (Conversation conversation)
+ {
+ Header h = conversation.getHeader();
+
+ boolean matches = true;
+ if (matches && author != null)
+ {
+ matches = h.getAuthors().contains(author);
+ }
+ if (matches && recipient != null)
+ {
+ matches = h.getRecipients().contains(recipient);
+ }
+ if (matches && subject != null)
+ {
+ matches = h.getSubject().equals(subject);
+ }
+ if (matches && (stateEquals != null || stateDiffers != null))
+ {
+ boolean equalMatch = stateEquals != null ? (h.getTransportState().hasOne(stateEquals)) : true;
+ boolean differMatch = stateDiffers != null ? (h.getTransportState().hasNone(stateDiffers)) : true;
+
+ matches = equalMatch && differMatch;
+ log.debug("matches filter ", stateEquals, ":!", stateDiffers, " : ", h.getTransportState(), " = ("+ equalMatch, "&", differMatch, ") = ", matches);
+ }
+ if (matches && bayesianDictionary != null && autoBayesian)
+ {
+ matches = bayesianMatches(conversation);
+ }
+
+ return matches;
+ }
+
+ public Dictionary getBayesianDictionary ()
+ {
+ return bayesianDictionary;
+ }
+
+ public FolderDefinition setBayesianDictionary(Dictionary bayesianDictionary)
+ {
+ this.bayesianDictionary = bayesianDictionary;
+ return this;
+ }
+
+ public FolderDefinition setAutoBayesian (boolean autoBayesian)
+ {
+ this.autoBayesian = autoBayesian;
+ return this;
+ }
+
+ public boolean bayesianMatches (Conversation conversation)
+ {
+ return bayesianDictionary.bayesianMatches(conversation.getHeader().getDictionary());
+ }
+
+ public boolean getAutoBayesian ()
+ {
+ return autoBayesian;
+ }
+
+ public void conversationAdded(Conversation conversation)
+ {
+ if (bayesianDictionary != null)
+ {
+ bayesianDictionary.add(conversation.getHeader().getDictionary());
+ }
+ }
+
+ public void conversationDeleted(Conversation conversation)
+ {
+ if (bayesianDictionary != null)
+ {
+ bayesianDictionary.subtract(conversation.getHeader().getDictionary());
+ }
+ }
+}
diff --git a/java/core/src/core/mail/client/model/FolderFilter.java b/java/core/src/core/mail/client/model/FolderFilter.java
new file mode 100644
index 0000000..aa9f336
--- /dev/null
+++ b/java/core/src/core/mail/client/model/FolderFilter.java
@@ -0,0 +1,67 @@
+/**
+ * Author: Timothy Prepscius
+ * License: GPLv3 Affero + keep my name in the code!
+ */
+package mail.client.model;
+
+import org.timepedia.exporter.client.NoExport;
+
+import mail.client.CacheManager;
+import mail.client.cache.Type;
+
+public class FolderFilter extends FolderSet
+{
+ @NoExport
+ public FolderFilter (CacheManager manager)
+ {
+ super(manager, Type.FolderPart);
+ }
+
+ @Override
+ protected void onLoaded()
+ {
+ super.onLoaded();
+ preCacheMostRecentFolder();
+ }
+
+ public boolean matchesFilter (Conversation conversation)
+ {
+ return true;
+ }
+
+ @Override
+ public synchronized void conversationAdded (Conversation conversation)
+ {
+ if (matchesFilter(conversation))
+ {
+ super.conversationAdded(conversation);
+ }
+ }
+
+ @Override
+ public synchronized void conversationDeleted (Conversation conversation)
+ {
+ if (hasConversation(conversation))
+ {
+ super.conversationDeleted(conversation);
+ }
+ }
+
+ public synchronized void manuallyAdd (Conversation conversation)
+ {
+ if (!super.hasConversation(conversation))
+ {
+ folderDefinition.conversationAdded(conversation);
+ super.conversationAdded(conversation);
+ }
+ }
+
+ public synchronized void manuallyRemove (Conversation conversation)
+ {
+ if (super.hasConversation(conversation))
+ {
+ folderDefinition.conversationDeleted(conversation);
+ super.conversationDeleted(conversation);
+ }
+ }
+}
diff --git a/java/core/src/core/mail/client/model/FolderFilterSet.java b/java/core/src/core/mail/client/model/FolderFilterSet.java
new file mode 100644
index 0000000..896b48b
--- /dev/null
+++ b/java/core/src/core/mail/client/model/FolderFilterSet.java
@@ -0,0 +1,18 @@
+/**
+ * Author: Timothy Prepscius
+ * License: GPLv3 Affero + keep my name in the code!
+ */
+package mail.client.model;
+
+import mail.client.CacheManager;
+import mail.client.cache.Type;
+
+public class FolderFilterSet extends FolderSet
+{
+
+ public FolderFilterSet(CacheManager manager)
+ {
+ super(manager, Type.FolderFilter);
+ }
+
+}
diff --git a/java/core/src/core/mail/client/model/FolderFilterSimple.java b/java/core/src/core/mail/client/model/FolderFilterSimple.java
new file mode 100644
index 0000000..9a4ca70
--- /dev/null
+++ b/java/core/src/core/mail/client/model/FolderFilterSimple.java
@@ -0,0 +1,27 @@
+/**
+ * Author: Timothy Prepscius
+ * License: GPLv3 Affero + keep my name in the code!
+ */
+package mail.client.model;
+
+import org.timepedia.exporter.client.Export;
+import org.timepedia.exporter.client.Exportable;
+import org.timepedia.exporter.client.NoExport;
+
+import mail.client.CacheManager;
+
+@Export()
+public class FolderFilterSimple extends FolderFilter
+{
+ @NoExport
+ public FolderFilterSimple(CacheManager manager)
+ {
+ super(manager);
+ }
+
+ @Override
+ public boolean matchesFilter (Conversation conversation)
+ {
+ return folderDefinition.matchesFilter(conversation);
+ }
+}
diff --git a/java/core/src/core/mail/client/model/FolderMaster.java b/java/core/src/core/mail/client/model/FolderMaster.java
new file mode 100644
index 0000000..0081300
--- /dev/null
+++ b/java/core/src/core/mail/client/model/FolderMaster.java
@@ -0,0 +1,88 @@
+/**
+ * Author: Timothy Prepscius
+ * License: GPLv3 Affero + keep my name in the code!
+ */
+package mail.client.model;
+
+import org.timepedia.exporter.client.Export;
+import org.timepedia.exporter.client.NoExport;
+
+
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+import core.util.Base64;
+import core.util.Strings;
+
+import core.crypt.HashSha256;
+
+import mail.client.CacheManager;
+import mail.client.cache.Type;
+
+@Export()
+public class FolderMaster extends FolderFilterSet
+{
+ HashSha256 hasher = new HashSha256();
+ Map externalKeys = new HashMap();
+ Map uidls = new HashMap();
+
+ @NoExport
+ public FolderMaster(CacheManager manager)
+ {
+ super(manager);
+ }
+
+ protected String hash (String key)
+ {
+ try
+ {
+ return Base64.encode(hasher.hash(Strings.toBytes(key)));
+ }
+ catch (Exception e)
+ {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public void addExternalKey (String id, Date date)
+ {
+ addExternalKeyHash(hash(id), date);
+ markDirty();
+ }
+
+ public void addExternalKeyHash (String hash, Date date)
+ {
+ externalKeys.put(hash, date);
+ }
+
+ public boolean containsExternalKey (String id)
+ {
+ return externalKeys.containsKey(hash(id));
+ }
+
+ public void addUIDL (String uidl, Date date)
+ {
+ addUIDLHash(hash(uidl), date);
+ markDirty();
+ }
+
+ public void addUIDLHash (String hash, Date date)
+ {
+ uidls.put(hash, date);
+ }
+
+ public boolean containsUIDL (String uidl)
+ {
+ return uidls.containsKey(hash(uidl));
+ }
+
+ public Map getUIDLHashes()
+ {
+ return uidls;
+ }
+
+ public Map getExternalKeyHashes ()
+ {
+ return externalKeys;
+ }
+}
diff --git a/java/core/src/core/mail/client/model/FolderPart.java b/java/core/src/core/mail/client/model/FolderPart.java
new file mode 100644
index 0000000..28bdd99
--- /dev/null
+++ b/java/core/src/core/mail/client/model/FolderPart.java
@@ -0,0 +1,155 @@
+/**
+ * Author: Timothy Prepscius
+ * License: GPLv3 Affero + keep my name in the code!
+ */
+package mail.client.model;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+
+import org.timepedia.exporter.client.NoExport;
+
+import core.util.Collectionz;
+import core.util.Comparators;
+import core.util.LogNull;
+import core.util.LogOut;
+import core.util.Pair;
+
+import mail.client.CacheManager;
+import mail.client.Events;
+import mail.client.cache.ID;
+
+public class FolderPart extends Folder
+{
+ static LogNull log = new LogNull(FolderPart.class);
+ static final int MAX_FOLDER_CONVERSATIONS = 1000;
+
+ List> conversations;
+
+ @NoExport
+ public FolderPart (CacheManager manager)
+ {
+ super(manager);
+ reset();
+
+ getLoadCallbacks()
+ .addCallback(manager.getMaster().getEventPropagator().signal_(Events.LoadFolderPart, this));
+ }
+
+ public void reset ()
+ {
+ conversations = new ArrayList>();
+ }
+
+ @Override
+ public List getConversations (int from, int length, String filter)
+ {
+ CacheManager cache = getManager();
+
+ log.debug("getConversations ", from, ":", length);
+
+ Dictionary filterDictionary = null;
+ if (filter != null)
+ filterDictionary = new Dictionary(filter);
+
+ log.debug("getConversations using filter dictionary", filterDictionary);
+
+ int min = Math.min(from+length, conversations.size());
+ List result = new ArrayList(min);
+
+ for (int i=from; i(id, date));
+ Collections.sort(conversations, new Comparators.SortBySecondNaturalOpposite());
+ }
+
+ protected synchronized void removeConversationId (Object id)
+ {
+ Collectionz.removeByFirst(conversations, id);
+ }
+
+ @Override
+ public List> getConversationIds()
+ {
+ return conversations;
+ }
+
+ @Override
+ public boolean isFull ()
+ {
+ return conversations.size() > MAX_FOLDER_CONVERSATIONS;
+ }
+
+ @Override
+ public synchronized void conversationAdded (Conversation conversation)
+ {
+ addConversationId(conversation.getId(), conversation.getHeader().getDate());
+ markDirty();
+ }
+
+ @Override
+ public synchronized void conversationDeleted (Conversation conversation)
+ {
+ removeConversationId(conversation.getId());
+ markDirty();
+ }
+
+ @Override
+ public Conversation getMatchingConversation (Header header)
+ {
+ if (header.getSubject() == null)
+ return null;
+
+ String headerSubject = header.getSubjectExcludingReplyPrefix ();
+
+ for (Pair pair : conversations)
+ {
+ ID id = pair.first;
+ Conversation conversation = getManager().getConversation(id);
+
+ if (conversation.isLoaded())
+ {
+ Header compare = conversation.getHeader();
+ String compareSubject = compare.getSubjectExcludingReplyPrefix();
+
+ if (compareSubject.toLowerCase().equals(headerSubject.toLowerCase()))
+ {
+ return conversation;
+ }
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/java/core/src/core/mail/client/model/FolderRepository.java b/java/core/src/core/mail/client/model/FolderRepository.java
new file mode 100644
index 0000000..b775bf7
--- /dev/null
+++ b/java/core/src/core/mail/client/model/FolderRepository.java
@@ -0,0 +1,29 @@
+/**
+ * Author: Timothy Prepscius
+ * License: GPLv3 Affero + keep my name in the code!
+ */
+package mail.client.model;
+
+import org.timepedia.exporter.client.Export;
+import org.timepedia.exporter.client.Exportable;
+import org.timepedia.exporter.client.NoExport;
+
+import mail.client.CacheManager;
+import mail.client.cache.Type;
+
+@Export()
+public class FolderRepository extends FolderSet
+{
+ @NoExport
+ public FolderRepository(CacheManager manager)
+ {
+ super(manager, Type.FolderPart);
+ }
+
+ @Override
+ protected void onLoaded()
+ {
+ super.onLoaded();
+ preCacheMostRecentFolder();
+ }
+}
diff --git a/java/core/src/core/mail/client/model/FolderSet.java b/java/core/src/core/mail/client/model/FolderSet.java
new file mode 100644
index 0000000..eca8b51
--- /dev/null
+++ b/java/core/src/core/mail/client/model/FolderSet.java
@@ -0,0 +1,246 @@
+/**
+ * Author: Timothy Prepscius
+ * License: GPLv3 Affero + keep my name in the code!
+ */
+package mail.client.model;
+
+import org.timepedia.exporter.client.Export;
+import org.timepedia.exporter.client.NoExport;
+
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+
+import core.callbacks.Single;
+import core.util.LogNull;
+import core.util.Pair;
+
+import mail.client.CacheManager;
+import mail.client.Events;
+import mail.client.cache.ID;
+import mail.client.cache.Type;
+
+@Export()
+public class FolderSet extends Folder
+{
+ static LogNull log = new LogNull(FolderSet.class);
+ List parts;
+ int numConversations;
+ Type childType;
+
+ @NoExport
+ public FolderSet(CacheManager manager, Type childType)
+ {
+ super(manager);
+ this.childType = childType;
+ reset();
+
+ getLoadCallbacks()
+ .addCallback(manager.getMaster().getEventPropagator().signal_(Events.LoadFolder, this));
+ }
+
+ public void preCacheMostRecentFolder ()
+ {
+ if (!parts.isEmpty())
+ {
+ CacheManager cache = getManager();
+ cache.getFolder(getChildType(), parts.get(0));
+ }
+ }
+
+ public void reset ()
+ {
+ parts = new ArrayList();
+ this.numConversations = 0;
+ }
+
+ public void addFolderId (ID id)
+ {
+ parts.add(0,id);
+ }
+
+ protected void addFolderIdEnd (ID id)
+ {
+ parts.add(id);
+ }
+
+ public List getFolderIds()
+ {
+ return parts;
+ }
+
+ public void addFolder (Folder folder)
+ {
+ addFolderIdEnd(folder.getId());
+ markDirty();
+ }
+
+ public void removeFolder(Folder folder)
+ {
+ if (!parts.contains(folder.getId()))
+ return;
+
+ parts.remove(folder.getId());
+ markDirty();
+
+ if (folder.isLoaded())
+ folder.markDeleted();
+ else
+ folder.getLoadCallbacks().addCallback(new Single(markDeleted_()));
+ }
+
+ protected void onDeleting ()
+ {
+ List parts = getFolders();
+ for(Folder part : parts)
+ removeFolder(part);
+ }
+
+ public List getFolders ()
+ {
+ List f = new ArrayList(parts.size());
+ for (ID id : parts)
+ f.add(getManager().getFolder(getChildType(), id));
+
+ return f;
+ }
+
+ @Override
+ public List getConversations(int from, int length, String filter)
+ {
+ log.debug("filter: ", filter);
+
+ int totalLength = from+length;
+ List result = new ArrayList(totalLength);
+
+ for (ID id : parts)
+ {
+ Folder f = getManager().getFolder(getChildType(), id);
+
+ if (f.isLoaded())
+ {
+ result.addAll(f.getConversations(0, totalLength - result.size(), filter));
+
+ if (result.size() >= totalLength)
+ break;
+ }
+ else
+ break;
+ }
+
+ totalLength = Math.min(totalLength, result.size());
+ return result.subList(from, totalLength);
+ }
+
+ @Override
+ public boolean hasConversation(Conversation conversation)
+ {
+ for (ID id : parts)
+ {
+ Folder f = getManager().getFolder(getChildType(), id);
+
+ if (f.isLoaded())
+ if (f.hasConversation(conversation))
+ return true;
+ }
+
+ return false;
+ }
+
+ public Type getChildType ()
+ {
+ return childType;
+ }
+
+ @Override
+ public void conversationAdded(Conversation conversation)
+ {
+ CacheManager cache = getManager();
+
+ Folder first = !parts.isEmpty() ? cache.getFolder(getChildType(), parts.get(0)) : null;
+
+ log.debug ("FolderSet.conversationAdded ", first);
+
+ if (parts.isEmpty() || !first.isLoaded() || first.isFull())
+ {
+ first = cache.newFolder(getChildType(), new FolderDefinition(getId().toFileSystemSafe() + ":part"));
+ parts.add(0, first.getId());
+ }
+
+ first.conversationAdded(conversation);
+
+ numConversations++;
+ markDirty();
+ }
+
+ @Override
+ public void conversationDeleted(Conversation conversation)
+ {
+ for (ID id : parts)
+ {
+ Folder f = getManager().getFolder(getChildType(), id);
+
+ if (f.isLoaded())
+ {
+ if (f.hasConversation(conversation))
+ {
+ f.conversationDeleted(conversation);
+ numConversations--;
+ markDirty();
+
+ break;
+ }
+ }
+ }
+
+ }
+
+ public int getNumConversations ()
+ {
+ return numConversations;
+ }
+
+ public void setNumConversations (int numConversations)
+ {
+ this.numConversations = numConversations;
+ }
+
+ @Override
+ public List> getConversationIds()
+ {
+ assert(false);
+ return null;
+ }
+
+ @Override
+ public void addConversationId(ID id, Date date)
+ {
+ assert(false);
+ }
+
+ @Override
+ public boolean isFull()
+ {
+ assert(false);
+ return false;
+ }
+
+ @Override
+ public Conversation getMatchingConversation (Header header)
+ {
+ for (ID id : parts)
+ {
+ Folder f = getManager().getFolder(getChildType(), id);
+
+ if (f.isLoaded())
+ {
+ Conversation c = f.getMatchingConversation(header);
+ if (c != null)
+ return c;
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/java/core/src/core/mail/client/model/Header.java b/java/core/src/core/mail/client/model/Header.java
new file mode 100644
index 0000000..3da40bf
--- /dev/null
+++ b/java/core/src/core/mail/client/model/Header.java
@@ -0,0 +1,325 @@
+/**
+ * Author: Timothy Prepscius
+ * License: GPLv3 Affero + keep my name in the code!
+ */
+package mail.client.model;
+
+import org.timepedia.exporter.client.Export;
+import org.timepedia.exporter.client.Exportable;
+
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import mail.client.Master;
+
+import core.util.DateFormat;
+import core.util.LogNull;
+import core.util.LogOut;
+import core.util.Strings;
+
+@Export()
+public class Header implements Exportable
+{
+ static LogNull log = new LogNull (Header.class);
+
+ protected String externalKey;
+ protected String originalKey;
+ protected Identity author;
+ protected Set authors;
+ protected String subject;
+ protected Date date;
+ protected Recipients recipients;
+ protected Dictionary dictionary;
+ protected TransportState state;
+ protected String brief;
+ protected String uidl;
+
+ public Header (String externalKey, String originalKey, String uidl, Identity author, Recipients recipients, String subject, Date date, TransportState state, String brief)
+ {
+ this.externalKey = externalKey;
+ this.originalKey = originalKey;
+ this.uidl = uidl;
+ this.author = author;
+ this.subject = subject;
+ this.date = date;
+ this.recipients = recipients;
+ this.state = state;
+ this.brief = brief;
+ }
+
+ public Header ()
+ {
+
+ }
+
+ public String getExternalKey ()
+ {
+ return externalKey;
+ }
+
+ public void setOriginalKey (String originalKey)
+ {
+ this.originalKey = originalKey;
+ }
+
+ public String getOriginalKey ()
+ {
+ return originalKey;
+ }
+
+ public void setExternalKey (String externalKey)
+ {
+ this.externalKey = externalKey;
+ }
+
+ public Identity getAuthor ()
+ {
+ return author;
+ }
+
+ public Identity[] filterMe (List identities, Identity me)
+ {
+ if (identities == null)
+ return null;
+
+ List filtered = new ArrayList();
+ for (Identity identity : identities)
+ {
+ if (identity != me)
+ {
+ log.debug("filterMe adding", identity.debug(), "is not",me.debug());
+ filtered.add(identity);
+ }
+ }
+
+ if (filtered.isEmpty())
+ return null;
+
+ return filtered.toArray(new Identity[0]);
+ }
+
+ public Identity[] calculateReplyTo (Master master)
+ {
+ log.debug("calculate replyTo");
+ Identity me = master.getIdentity();
+
+ Identity[] results = null;
+ if (getRecipients()!=null)
+ {
+ log.debug("recipients not null");
+
+ results = filterMe(getRecipients().getReplyTo(), me);
+ if (results != null)
+ return results;
+
+ if (getAuthor() == me)
+ {
+ results = filterMe(getRecipients().getAll(), me);
+ if (results != null)
+ return results;
+ }
+ }
+
+ if (getAuthor() != null)
+ return new Identity[] { getAuthor() };
+
+ return new Identity[0];
+ }
+
+ public Identity[] calculateReplyAll (Master master)
+ {
+ Identity me = master.getIdentity();
+ ArrayList results = new ArrayList();
+
+ if (getRecipients()!=null)
+ {
+ Identity[] filtered = filterMe(getRecipients().getAll(), me);
+ if (filtered != null)
+ for (Identity identity : filtered)
+ results.add(identity);
+ }
+
+ Identity author = getAuthor();
+ if (author!=null && author != me && !results.contains(author))
+ results.add(0, author);
+
+ return results.toArray(new Identity[0]);
+ }
+
+ public void setAuthor (Identity author)
+ {
+ this.author = author;
+ }
+
+ public Set getAuthors ()
+ {
+ return authors;
+ }
+
+ public void setAuthors (List authors)
+ {
+ this.authors = new HashSet();
+ this.authors.addAll(authors);
+ }
+
+ public String getAuthorsShortList ()
+ {
+ String[] shorts = new String[authors.size()];
+ int j=0;
+ for (Identity i : authors)
+ shorts[j++] = i.getShortName();
+
+ return Strings.concat(shorts, ", ");
+ }
+
+ public String getSubject ()
+ {
+ return subject;
+ }
+
+ public String getSubjectExcludingReplyPrefix ()
+ {
+ String subject = this.subject;
+
+ if (subject == null)
+ subject = "";
+
+ while (subject.toLowerCase().startsWith(ConstantsMisc.REPLY_PREFIX.toLowerCase()))
+ {
+ subject = subject.substring(ConstantsMisc.REPLY_PREFIX.length()).trim();
+ }
+
+ return subject;
+ }
+
+ public Dictionary getDictionary ()
+ {
+ return dictionary;
+ }
+
+ public void setDictionary (Dictionary dictionary)
+ {
+ this.dictionary = dictionary;
+ }
+
+ public Date getDate ()
+ {
+ return date;
+ }
+
+ public void setDate (Date date)
+ {
+ this.date = date;
+ }
+
+ public Recipients getRecipients ()
+ {
+ return recipients;
+ }
+
+ public void setRecipients (Recipients recipients)
+ {
+ this.recipients = recipients;
+ }
+
+ public void setSubject(String subject)
+ {
+ this.subject = subject;
+ }
+
+ public void setTransportState (TransportState state)
+ {
+ this.state = state;
+ }
+
+ public TransportState getTransportState ()
+ {
+ return state;
+ }
+
+ public boolean hasState(String flag)
+ {
+ return state.has(flag);
+ }
+
+ public void markState (String flag)
+ {
+ state.mark(flag);
+ }
+
+ public void unmarkState(String flag)
+ {
+ state.unmark(flag);
+ }
+
+ public void markModification ()
+ {
+ this.date = new Date();
+ }
+
+ public String getBrief()
+ {
+ return brief;
+ }
+
+ public void setBrief(String brief)
+ {
+ this.brief = brief;
+ }
+
+ public String getRelativeDate ()
+ {
+ String result = "Unknown";
+
+ if (date == null)
+ result = "Infinity + 1";
+ else
+ {
+ DateFormat std = new DateFormat("yyyyMMddhhmmss");
+ String now = std.format(new Date());
+ String then = std.format(date);
+
+ if (then.startsWith(now.substring(0,10)))
+ {
+ long diff = (new Date().getTime()/(60 * 1000)) - (date.getTime()/(60 * 1000));
+ result = diff + " min";
+ }
+ else
+ if (then.startsWith(now.substring(0,8)))
+ {
+ DateFormat sdf = new DateFormat("h:mm a");
+ result = sdf.format(date).toLowerCase();
+ }
+ else
+ {
+ if (then.startsWith(now.substring(0,4)))
+ {
+ DateFormat sdf = new DateFormat("MMM d");
+ result = sdf.format(date);
+ }
+ else
+ {
+ DateFormat sdf = new DateFormat("MM/dd/yy");
+ result = sdf.format(date);
+ }
+ }
+ }
+
+ log.debug("Header.getRelativeDate ", result);
+ return result;
+ }
+
+ public String getUIDL()
+ {
+ return uidl;
+ }
+
+ public void setUIDL (String uidl)
+ {
+ this.uidl = uidl;
+ }
+}
diff --git a/java/core/src/core/mail/client/model/Identity.java b/java/core/src/core/mail/client/model/Identity.java
new file mode 100644
index 0000000..8f46a21
--- /dev/null
+++ b/java/core/src/core/mail/client/model/Identity.java
@@ -0,0 +1,187 @@
+/**
+ * Author: Timothy Prepscius
+ * License: GPLv3 Affero + keep my name in the code!
+ */
+package mail.client.model;
+
+import org.timepedia.exporter.client.Export;
+import org.timepedia.exporter.client.Exportable;
+
+
+import java.io.Serializable;
+
+import core.util.LogNull;
+import core.util.LogOut;
+import core.util.Pair;
+import core.util.Strings;
+
+@Export()
+public class Identity implements Serializable, Exportable
+{
+ private static final long serialVersionUID = 1L;
+ static LogNull log = new LogNull(Identity.class);
+
+ String name;
+ String email;
+ boolean isPrimary = false;
+ String publicKey;
+
+ protected Identity ()
+ {
+ }
+
+ protected Identity (String name, String email, boolean isPrimary)
+ {
+ this.name = name;
+ this.email = email;
+ this.isPrimary = isPrimary;
+ }
+
+ /**
+ * Rewrite with regular expressions
+ * @param full
+ */
+ public Identity (String full)
+ {
+ Pair parsed = parseFull(full);
+ this.name = parsed.first;
+ this.email = parsed.second;
+ }
+
+ public void setPrimary (boolean primary)
+ {
+ this.isPrimary = primary;
+ }
+
+ public static Pair parseFull (String full)
+ {
+ String name = null;
+ String email = null;
+
+ int indexOfLessThan = full.lastIndexOf('<');
+ int indexOfGreaterThan = full.indexOf('>', indexOfLessThan);
+ if (indexOfLessThan != -1 && indexOfGreaterThan != -1)
+ {
+ name = full.substring(0, indexOfLessThan);
+ name = name.trim();
+ if (name.isEmpty())
+ name = null;
+
+ String t = full.substring(indexOfLessThan+1);
+ email = t.substring(0, indexOfGreaterThan - indexOfLessThan - 1);
+ }
+ else
+ {
+ email = full;
+ }
+
+ return new Pair(name, email);
+ }
+
+ public String getFull()
+ {
+ if (name != null && !name.isEmpty())
+ return name + " " + getEnclosedEmail();
+
+ return getEnclosedEmail();
+ }
+
+ public String toString()
+ {
+ return getFull();
+ }
+
+ public String debug ()
+ {
+ return super.toString() + getFull();
+ }
+
+ public String getName()
+ {
+ return name;
+ }
+
+ public void setName(String name)
+ {
+ this.name = name;
+ }
+
+ public String getEmail()
+ {
+ return email;
+ }
+
+ public String getEnclosedEmail ()
+ {
+ if (email != null)
+ return "<" + email + ">";
+
+ return null;
+ }
+
+ public void setEmail(String email)
+ {
+ this.email = email;
+ }
+
+ public String getShortName ()
+ {
+ if (isPrimary)
+ return "me";
+
+ if (name != null)
+ if (name.contains(" "))
+ if (name.contains(","))
+ return Strings.trimQuotes(name.substring(name.indexOf(' ')+1));
+ else
+ return Strings.trimQuotes(name.substring(0, name.indexOf(' ')));
+ else
+ return name;
+
+ return email;
+ }
+
+ public String getLongName ()
+ {
+ return name;
+ }
+
+ /**
+ * This should check which information is better,
+ * possibly based on amount of information in the information?
+ * @param identity
+ */
+ public void copyFrom (Identity identity)
+ {
+ if (!this.isPrimary)
+ {
+ log.debug("copying from ", identity.isPrimary, identity.name, identity.email, this.isPrimary, this.name, this.email);
+
+ if (
+ this.name == null ||
+ ( (identity.name != null) && (this.name.length() < identity.name.length()) )
+ )
+ {
+ this.name = identity.name;
+ }
+
+ if (this.email == null)
+ this.email = identity.email;
+ }
+ }
+
+ public void setPublicKey (String publicKey)
+ {
+ this.publicKey = publicKey;
+ }
+
+ public boolean hasPublicKey()
+ {
+ return publicKey != null;
+ }
+
+ public String getPublicKey()
+ {
+ return publicKey;
+ }
+}
diff --git a/java/core/src/core/mail/client/model/Mail.java b/java/core/src/core/mail/client/model/Mail.java
new file mode 100644
index 0000000..007688c
--- /dev/null
+++ b/java/core/src/core/mail/client/model/Mail.java
@@ -0,0 +1,197 @@
+/**
+ * Author: Timothy Prepscius
+ * License: GPLv3 Affero + keep my name in the code!
+ */
+package mail.client.model;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.List;
+
+import com.sun.org.apache.xml.internal.security.utils.Base64;
+
+import sun.reflect.ReflectionFactory.GetReflectionFactoryAction;
+
+import mail.client.CacheManager;
+import mail.client.Events;
+
+import core.constants.ConstantsMailJson;
+import core.crypt.CryptorAES;
+import core.crypt.CryptorRSAAES;
+import core.crypt.CryptorRSAFactory;
+import core.util.JSON_;
+import core.util.LogNull;
+import core.util.Pair;
+import core.util.Strings;
+
+public class Mail extends Model
+{
+ static LogNull log = new LogNull(Mail.class);
+
+ static class SortByDateLatestFirst implements Comparator {
+
+ @Override
+ public int compare(Mail l, Mail r)
+ {
+ Date ld = l.getHeader().getDate(), rd = r.getHeader().getDate();
+ return rd.compareTo(ld);
+ }
+ }
+
+ static class SortByDateLatestLast implements Comparator {
+
+ @Override
+ public int compare(Mail l, Mail r)
+ {
+ Date ld = l.getHeader().getDate(), rd = r.getHeader().getDate();
+ return ld.compareTo(rd);
+ }
+ }
+
+ Header header;
+ Body body;
+ Attachments attachments;
+
+ public Mail (CacheManager manager)
+ {
+ super(manager);
+
+ getLoadCallbacks()
+ .addCallback(manager.getMaster().getEventPropagator().signal_(Events.LoadMail, this));
+ }
+
+ public void reset ()
+ {
+
+ }
+
+ public Header getHeader ()
+ {
+ return header;
+ }
+
+ public void setHeader (Header header)
+ {
+ this.header = header;
+ }
+
+ public Body getBody ()
+ {
+ return body;
+ }
+
+ public void setBody (Body body)
+ {
+ this.body = body;
+ }
+
+ public void setBody (String text, String html)
+ {
+ body.setText(text);
+ body.setHTML(html);
+ header.setBrief(body.calculateBrief());
+ }
+
+ public Identity[] calculateReplyTo ()
+ {
+ return header.calculateReplyTo(getManager().getMaster());
+ }
+
+ public Body calculateReply (String signature)
+ {
+ Body reply = new Body();
+
+ String author = "" + getHeader().getAuthor();
+ String date = "" + getHeader().getDate();
+
+ String prefix = "On " + date + ", " + author + " wrote:";
+ String html =
+ "
" + signature + "
" + prefix + "
" +
+ "" +
+ (body.hasHTML() ? body.getStrippedHTML() : ("" + body.getStrippedText() + "
")) + "
";
+
+ reply.setText("\r\n\r\n" + signature + "\r\n\r\n" + prefix + "\r\n" + body.calculateReply());
+ reply.setHTML(html);
+
+ return reply;
+ }
+
+ public Attachments getAttachments ()
+ {
+ return attachments;
+ }
+
+ public void setAttachments(Attachments attachments)
+ {
+ this.attachments = attachments;
+ }
+
+ public boolean isPresendEncryptable()
+ {
+ for (Identity i : getHeader().getRecipients().getAll())
+ {
+ if (!i.hasPublicKey())
+ return false;
+ }
+
+ return true;
+ }
+
+ public Pair presendEncrypt () throws Exception
+ {
+ byte[] aesKey = CryptorAES.newKey();
+ CryptorAES aes = new CryptorAES(aesKey);
+
+ List cryptors = new ArrayList();
+ for (Identity i : getHeader().getRecipients().getAll())
+ {
+ CryptorRSAAES rsaaes = new CryptorRSAAES(CryptorRSAFactory.fromString(i.getPublicKey(), null));
+ cryptors.add(Base64.encode(rsaaes.encrypt(aesKey)));
+ }
+
+ Object json = JSON_.newObject();
+ JSON_.put(json, ConstantsMailJson.Class, JSON_.newString(ConstantsMailJson.MultiPart));
+ Object multiPart = JSON_.newArray();
+
+
+ if (getBody().hasText())
+ {
+ Object part = JSON_.newObject();
+ Object headers = JSON_.newArray();
+ Object mimeType = JSON_.newArray();
+ JSON_.add(mimeType, JSON_.newString("Content-Type"));
+ JSON_.add(mimeType, JSON_.newString("text/plain"));
+ JSON_.add(headers, mimeType);
+ JSON_.put(part, ConstantsMailJson.Class, ConstantsMailJson.String);
+ JSON_.put(part, ConstantsMailJson.Value, getBody().getText());
+ JSON_.put(part, ConstantsMailJson.Headers, headers);
+
+ JSON_.add(multiPart, part);
+ }
+
+ {
+ Object part = JSON_.newObject();
+ Object headers = JSON_.newArray();
+ Object mimeType = JSON_.newArray();
+ JSON_.add(mimeType, JSON_.newString("Content-Type"));
+ JSON_.add(mimeType, JSON_.newString("text/html"));
+ JSON_.add(headers, mimeType);
+ JSON_.put(part, ConstantsMailJson.Class, ConstantsMailJson.String);
+ JSON_.put(part, ConstantsMailJson.Value, getBody().getHTML());
+ JSON_.put(part, ConstantsMailJson.Headers, headers);
+
+ JSON_.add(multiPart, part);
+ }
+
+ Object container = JSON_.newObject();
+ JSON_.put(container, "subject", getHeader().getSubject());
+ JSON_.put(container, "content", multiPart);
+
+ return new Pair(
+ Strings.concat(cryptors,","),
+ Base64.encode(aes.encrypt(Strings.toBytes(JSON_.asString(container))))
+ );
+ }
+}
diff --git a/java/core/src/core/mail/client/model/Model.java b/java/core/src/core/mail/client/model/Model.java
new file mode 100644
index 0000000..6df9cb3
--- /dev/null
+++ b/java/core/src/core/mail/client/model/Model.java
@@ -0,0 +1,38 @@
+/**
+ * Author: Timothy Prepscius
+ * License: GPLv3 Affero + keep my name in the code!
+ */
+package mail.client.model;
+
+import mail.client.CacheManager;
+
+public class Model extends mail.client.cache.Item
+{
+ CacheManager manager;
+
+ public Model(CacheManager manager)
+ {
+ this.manager = manager;
+ }
+
+ public CacheManager getManager ()
+ {
+ return manager;
+ }
+
+ protected void onPreLoad ()
+ {
+ reset();
+ }
+
+ protected void onDirty ()
+ {
+ super.onDirty();
+ manager.onModelDirty();
+ }
+
+ public void reset ()
+ {
+
+ }
+}
diff --git a/java/core/src/core/mail/client/model/ModelFactory.java b/java/core/src/core/mail/client/model/ModelFactory.java
new file mode 100644
index 0000000..200fb94
--- /dev/null
+++ b/java/core/src/core/mail/client/model/ModelFactory.java
@@ -0,0 +1,44 @@
+/**
+ * Author: Timothy Prepscius
+ * License: GPLv3 Affero + keep my name in the code!
+ */
+package mail.client.model;
+
+import mail.client.CacheManager;
+import mail.client.cache.Item;
+import mail.client.cache.ItemFactory;
+import mail.client.cache.Type;
+
+public class ModelFactory implements ItemFactory
+{
+ CacheManager manager;
+
+ public ModelFactory (CacheManager manager)
+ {
+ this.manager = manager;
+ }
+
+ @Override
+ public Item instantiate (Type type)
+ {
+ switch (type)
+ {
+ case Mail:
+ return new Mail(manager);
+ case Conversation:
+ return new Conversation(manager);
+ case FolderPart:
+ return new FolderPart(manager);
+ case FolderFilterSet:
+ return new FolderFilterSet(manager);
+ case FolderMaster:
+ return new FolderMaster(manager);
+ case FolderFilter:
+ return new FolderFilterSimple(manager);
+ case FolderRepository:
+ return new FolderRepository(manager);
+ }
+
+ return null;
+ }
+}
diff --git a/java/core/src/core/mail/client/model/ModelSerializer.java b/java/core/src/core/mail/client/model/ModelSerializer.java
new file mode 100644
index 0000000..15060a5
--- /dev/null
+++ b/java/core/src/core/mail/client/model/ModelSerializer.java
@@ -0,0 +1,86 @@
+/**
+ * Author: Timothy Prepscius
+ * License: GPLv3 Affero + keep my name in the code!
+ */
+package mail.client.model;
+
+import core.callback.Callback;
+import core.callback.CallbackDefault;
+import core.util.JSON_;
+import core.util.LogNull;
+import core.util.LogOut;
+import core.util.Strings;
+import core.util.Zip;
+import mail.client.cache.Item;
+import mail.client.cache.ItemSerializer;
+import mail.client.cache.JSON;
+
+public class ModelSerializer implements ItemSerializer
+{
+ static LogNull log = new LogNull(ModelSerializer.class);
+
+ JSON json;
+
+ public ModelSerializer(JSON json)
+ {
+ this.json = json;
+ }
+
+ public byte[] serialize (Item item) throws Exception
+ {
+ log.debug("serialize", item);
+
+ item.markPreStore();
+
+ if (item instanceof Mail)
+ return Strings.toBytes(json.toJSON((Mail)item).toString());
+ if (item instanceof Conversation)
+ return Strings.toBytes(json.toJSON((Conversation)item).toString());
+ if (item instanceof Folder)
+ return Strings.toBytes(json.toJSON((Folder)item).toString());
+ if (item instanceof Settings)
+ return Strings.toBytes(json.toJSON((Settings)item).toString());
+
+ return null;
+ }
+
+ public void deserialize (Item item, byte[] bytes) throws Exception
+ {
+ log.debug("deserialize", item);
+
+ item.markPreLoad();
+
+ if (item instanceof Mail)
+ json.fromJSON((Mail)item, JSON_.parse(Strings.toString(bytes)));
+ if (item instanceof Conversation)
+ json.fromJSON((Conversation)item, JSON_.parse(Strings.toString(bytes)));
+ if (item instanceof Folder)
+ json.fromJSON((Folder)item, JSON_.parse(Strings.toString(bytes)));
+ if (item instanceof Settings)
+ json.fromJSON((Settings)item, JSON_.parse(Strings.toString(bytes)));
+ }
+
+ @Override
+ public Callback serialize_(Item item)
+ {
+ return new CallbackDefault(item) {
+ public void onSuccess(Object... arguments) throws Exception {
+ next(serialize((Item)V(0)));
+ }
+ }.addCallback(Zip.deflate_());
+ }
+
+ @Override
+ public Callback deserialize_(Item item)
+ {
+ return Zip.inflate_().addCallback(
+ new CallbackDefault(item) {
+ public void onSuccess(Object... arguments) throws Exception {
+ deserialize((Item)V(0), (byte[])arguments[0]);
+ next();
+ }
+ }
+ );
+ }
+
+}
diff --git a/java/core/src/core/mail/client/model/Original.java b/java/core/src/core/mail/client/model/Original.java
new file mode 100644
index 0000000..b5ed4a6
--- /dev/null
+++ b/java/core/src/core/mail/client/model/Original.java
@@ -0,0 +1,78 @@
+/**
+ * Author: Timothy Prepscius
+ * License: GPLv3 Affero + keep my name in the code!
+ */
+package mail.client.model;
+
+import java.io.UnsupportedEncodingException;
+import java.util.Date;
+import java.util.List;
+
+import org.timepedia.exporter.client.Export;
+import org.timepedia.exporter.client.Exportable;
+
+import core.util.LogNull;
+
+@Export()
+public class Original implements Exportable
+{
+ static LogNull log = new LogNull(Original.class);
+
+ String path;
+ boolean loaded;
+
+ Exception exception;
+ byte[] data;
+
+ public Original (String path)
+ {
+ this.path = path;
+ }
+
+ public String getPath ()
+ {
+ return path;
+ }
+
+ public void setData (byte[] data)
+ {
+ this.data = data;
+ this.loaded = true;
+ }
+
+ public boolean hasData ()
+ {
+ return data!=null;
+ }
+
+ public String getDataAsString () throws UnsupportedEncodingException
+ {
+ return new String(data);
+ }
+
+ public boolean isLoaded ()
+ {
+ return loaded;
+ }
+
+ public boolean hasException()
+ {
+ return exception!=null;
+ }
+
+ public void setException(Exception exception)
+ {
+ this.exception = exception;
+ this.loaded = true;
+ }
+
+ public Exception getException()
+ {
+ return exception;
+ }
+
+ public boolean equals (Original rhs)
+ {
+ return super.equals(rhs);
+ }
+}
diff --git a/java/core/src/core/mail/client/model/Original.java.no b/java/core/src/core/mail/client/model/Original.java.no
new file mode 100644
index 0000000..5091e20
--- /dev/null
+++ b/java/core/src/core/mail/client/model/Original.java.no
@@ -0,0 +1,149 @@
+/**
+ * Author: Timothy Prepscius
+ * License: GPLv3 Affero + keep my name in the code!
+ */
+package mail.client.core;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.List;
+import java.util.Properties;
+
+import javax.mail.BodyPart;
+import javax.mail.MessagingException;
+import javax.mail.Session;
+import javax.mail.internet.MimeMessage;
+import javax.mail.internet.MimeMultipart;
+
+import com.sun.mail.util.BASE64DecoderStream;
+
+import core.util.Arrays;
+import core.util.LogNull;
+import core.util.LogNull;
+import core.util.Pair;
+import core.util.Streams;
+import core.util.Triple;
+
+public class Original
+{
+ static LogNull log = new LogNull(Original.class);
+
+ String path;
+ boolean loaded;
+
+ Exception exception;
+ byte[] data;
+
+ public Original (String path)
+ {
+ this.path = path;
+ }
+
+ public String getPath ()
+ {
+ return path;
+ }
+
+ public void setData (byte[] data)
+ {
+ this.data = data;
+ this.loaded = true;
+ }
+
+ public boolean hasData ()
+ {
+ return data!=null;
+ }
+
+ public String getDataAsString () throws UnsupportedEncodingException
+ {
+ return new String(data);
+ }
+
+ public boolean isLoaded ()
+ {
+ return loaded;
+ }
+
+ public boolean hasException()
+ {
+ return exception!=null;
+ }
+
+ public void setException(Exception exception)
+ {
+ this.exception = exception;
+ this.loaded = true;
+ }
+
+ public Exception getException()
+ {
+ return exception;
+ }
+
+ List attachments;
+
+ public void loadAttachments (Attachments attachments) throws Exception
+ {
+ log.debug("loadAttachments a ", data.length, " "+ new Date());
+
+ Session s = Session.getDefaultInstance(new Properties());
+ MimeMessage message = new MimeMessage(s, new ByteArrayInputStream(data));
+
+ log.debug("loadAttachments b ", new Date());
+ List> contents = new ArrayList>();
+ contents.add(new Triple(message.getDisposition(), message.getContentID(), message.getContent()));
+ while (contents.size() > 0)
+ {
+ log.debug("loadAttachments c ", new Date());
+
+ Triple p = contents.get(0);
+ contents.remove(0);
+
+ String disposition = p.first;
+ String id = p.second;
+ Object content = p.third;
+
+ if (content instanceof InputStream)
+ {
+ String attachmentId = Attachment.getAttachmentId(disposition, id);
+
+ Attachment attachment = attachments.getAttachment(attachmentId);
+ if (attachment != null)
+ {
+ attachment.setData (Streams.readFullyBytes((InputStream)content));
+ }
+ }
+ else
+ if (content instanceof MimeMultipart)
+ {
+ MimeMultipart m = (MimeMultipart)content;
+ for (int i=0; i(
+ Arrays.firstOrNull(b.getHeader("Content-Disposition")),
+ Arrays.firstOrNull(b.getHeader("Content-Id")),
+ b.getContent()
+ )
+ );
+ }
+ }
+ }
+
+ log.debug("loadAttachments d ", new Date());
+ attachments.setLoaded(true);
+ }
+
+ public List getAttachments ()
+ {
+ log.debug("getAttachments", new Date());
+ return attachments;
+ }
+}
diff --git a/java/core/src/core/mail/client/model/Recipients.java b/java/core/src/core/mail/client/model/Recipients.java
new file mode 100644
index 0000000..6ac440a
--- /dev/null
+++ b/java/core/src/core/mail/client/model/Recipients.java
@@ -0,0 +1,207 @@
+/**
+ * Author: Timothy Prepscius
+ * License: GPLv3 Affero + keep my name in the code!
+ */
+package mail.client.model;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.timepedia.exporter.client.Export;
+import org.timepedia.exporter.client.Exportable;
+
+import mail.client.Master;
+
+import core.util.Strings;
+
+@Export()
+public class Recipients implements Exportable
+{
+ public static final String To = "to", Cc = "cc", Bcc = "bcc", ReplyTo = "reply-to";
+
+ protected List to, cc, bcc, replyTo;
+
+ List toIdentityList (Master master, String is)
+ {
+ AddressBook addressBook = master.getAddressBook();
+
+ String[] parts = is.split(",");
+ List identities = new ArrayList(parts.length);
+
+ for (String part : parts)
+ {
+ String trimmed = part.trim();
+ if (trimmed.isEmpty())
+ continue;
+
+ Identity identity = addressBook.getIdentity(new UnregisteredIdentity(trimmed));
+ if (!identities.contains(identity))
+ identities.add(identity);
+ }
+
+ return identities;
+ }
+
+ public Recipients ()
+ {
+ this.to = new ArrayList();
+ this.cc = new ArrayList();
+ this.bcc = new ArrayList();
+ this.replyTo = new ArrayList();
+ }
+
+ public Recipients (Identity[] to, Identity[] cc, Identity[] bcc, Identity[] replyTo)
+ {
+ this.to = new ArrayList();
+ this.cc = new ArrayList();
+ this.bcc = new ArrayList();
+ this.replyTo = new ArrayList();
+
+ if (to != null)
+ for (Identity i : to)
+ this.to.add(i);
+
+ if (cc != null)
+ for (Identity i : cc)
+ this.cc.add(i);
+
+ if (bcc != null)
+ for (Identity i : bcc)
+ this.bcc.add(i);
+
+ if (replyTo != null)
+ for (Identity i : replyTo)
+ this.replyTo.add(i);
+
+ }
+
+ public void add (List from, List to)
+ {
+ for (Identity i : from)
+ if (!to.contains(i))
+ to.add(i);
+ }
+
+ public void add (Recipients r)
+ {
+ add (r.to, to);
+ add (r.cc, cc);
+ add (r.bcc, bcc);
+ add (r.replyTo, replyTo);
+ }
+
+ public List get(String key)
+ {
+ if (key.equals(To))
+ return to;
+ else
+ if (key.equals(Cc))
+ return cc;
+ else
+ if (key.equals(Bcc))
+ return bcc;
+ else
+ if (key.equals(ReplyTo))
+ return replyTo;
+
+ return null;
+ }
+
+ public List getTo()
+ {
+ return to;
+ }
+
+ public void setTo (List to)
+ {
+ this.to = to;
+ }
+
+ public List getCc ()
+ {
+ return cc;
+ }
+
+ public void setCc (List cc)
+ {
+ this.cc = cc;
+ }
+
+ public List getBcc ()
+ {
+ return bcc;
+ }
+
+ public void setBcc (List bcc)
+ {
+ this.bcc = bcc;
+ }
+
+ public List getReplyTo ()
+ {
+ return replyTo;
+ }
+
+ public void setReplyTo (List replyTo)
+ {
+ this.replyTo = replyTo;
+ }
+
+ public List getAll ()
+ {
+ ArrayList all = new ArrayList();
+ all.addAll(to);
+ all.addAll(cc);
+ all.addAll(bcc);
+ all.addAll(replyTo);
+
+ ArrayList once = new ArrayList();
+ for (Identity identity : all)
+ {
+ if (!once.contains(all))
+ once.add(identity);
+ }
+
+ return once;
+ }
+
+ protected void registerRecipients (AddressBook addressBook, List to)
+ {
+ Identity[] save = to.toArray(new Identity[0]);
+ to.clear();
+
+ for (Identity i : save)
+ {
+ if (i instanceof UnregisteredIdentity)
+ to.add(addressBook.getIdentity((UnregisteredIdentity)i));
+ else
+ to.add(i);
+ }
+ }
+
+ public void registerRecipients (AddressBook addressBook)
+ {
+ registerRecipients (addressBook, to);
+ registerRecipients (addressBook, cc);
+ registerRecipients (addressBook, bcc);
+ registerRecipients (addressBook, replyTo);
+ }
+
+ public boolean contains (Identity identity)
+ {
+ return
+ to.contains(identity) ||
+ cc.contains(identity) ||
+ bcc.contains(identity) ||
+ replyTo.contains(identity);
+ }
+
+ public String shortList ()
+ {
+ List shorts = new ArrayList();
+ for (Identity i : getAll())
+ shorts.add(i.getShortName());
+
+ return Strings.concat(shorts.iterator(), ", ");
+ }
+}
diff --git a/java/core/src/core/mail/client/model/Settings.java b/java/core/src/core/mail/client/model/Settings.java
new file mode 100644
index 0000000..fceec57
--- /dev/null
+++ b/java/core/src/core/mail/client/model/Settings.java
@@ -0,0 +1,77 @@
+/**
+ * Author: Timothy Prepscius
+ * License: GPLv3 Affero + keep my name in the code!
+ */
+package mail.client.model;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.timepedia.exporter.client.Export;
+import org.timepedia.exporter.client.Exportable;
+
+import mail.client.CacheManager;
+import mail.client.cache.Item;
+
+@Export()
+public class Settings extends Model implements Exportable
+{
+ public static final String
+ VERSION = "version",
+ CURRENT_VERSION = "1.0";
+
+ protected Map