/*
* Funambol is a mobile platform developed by Funambol, Inc.
* Copyright (C) 2003 - 2007 Funambol, Inc.
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License version 3 as published by
* the Free Software Foundation with the addition of the following permission
* added to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED
* WORK IN WHICH THE COPYRIGHT IS OWNED BY FUNAMBOL, FUNAMBOL DISCLAIMS THE
* WARRANTY OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program; if not, see http://www.gnu.org/licenses or write to
* the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
* MA 02110-1301 USA.
*
* You can contact Funambol, Inc. headquarters at 643 Bair Island Road, Suite
* 305, Redwood City, CA 94063, USA, or at email address info@funambol.com.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU Affero General Public License version 3.
*
* In accordance with Section 7(b) of the GNU Affero General Public License
* version 3, these Appropriate Legal Notices must retain the display of the
* "Powered by Funambol" logo. If the display of the logo is not reasonably
* feasible for technical reasons, the Appropriate Legal Notices must display
* the words "Powered by Funambol".
*/
package com.funambol.syncclient.sps;
import java.util.Enumeration;
import java.util.Vector;
import javax.microedition.rms.RecordStore;
import com.funambol.syncclient.blackberry.StringVector;
import com.funambol.syncclient.blackberry.email.impl.Constants;
import com.funambol.syncclient.blackberry.email.impl.MailParser;
import com.funambol.syncclient.blackberry.email.impl.MessageIDStore;
import com.funambol.syncclient.blackberry.email.impl.StringUtil;
import com.funambol.syncclient.blackberry.email.impl.XMLMailParser;
import com.funambol.syncclient.blackberry.parser.ParserFactory;
import com.funambol.syncclient.common.StringTools;
import com.funambol.syncclient.util.PagedVector;
import com.funambol.syncclient.util.StaticDataHelper;
import net.rim.device.api.system.PersistentObject;
import net.rim.device.api.system.PersistentStore;
import net.rim.device.api.util.StringMatch;
import net.rim.blackberry.api.mail.Message;
import net.rim.blackberry.api.mail.Folder;
import net.rim.blackberry.api.mail.FolderNotFoundException;
import net.rim.blackberry.api.mail.Store;
import net.rim.blackberry.api.mail.Session;
/**
* Processes and stores mail related operations and data
*
*/
public class MailDataStore extends DataStore
{
private static long MAIL_LAST_TIMESTAMP_KEY = 0xb56575eda53298eL;
private static final int ALERT_CODE_FAST = 200;
private static final int ALERT_CODE_SLOW = 201;
private static PersistentObject mailStore;
//private static PersistentObject store;
//private static Vector changes;//NOT USED
private PagedVector modifiedItems;
private MailParser mailParser = null;
private static PersistentObject persist = null;
private Vector messageVector = null;
//
static
{
/*
* The definition of the constant PERSISTENCE_KEY is in the
* abstract class DataStore (0x50b137116d9be33cL). The current
* value is inherited from the abstract class. It seems, this
* persistent object 'store' is nowhere used in the code, as
* well as the vector 'changes'
*/
// store = PersistentStore.getPersistentObject(PERSISTENCE_KEY);
// if (store.getContents() == null)
// {
// store.setContents(new Vector());
// store.commit();
// }
// changes = (Vector)store.getContents();
}
public MailDataStore(int page)
{
this();
setPage(page);
}
public MailDataStore()
{
super();
modifiedItems = null;
mailParser = ParserFactory.getParserInstance();
}
/**
* Sets the last timestamp into the dedicated record store
* @param lastTimestamp
* @throws DataAccessException
*/
public void setLastTimestamp(long lastTimestamp) throws DataAccessException
{
// FIXME: use J2ME RecordStore here
mailStore = PersistentStore.getPersistentObject(MAIL_LAST_TIMESTAMP_KEY);
StringVector data = new StringVector();
data.insertElementAt(Long.toString(lastTimestamp), 0);
mailStore.setContents(data);
mailStore.commit();
}
/**
* Returns the last timestamp from the dedicated record store
*
* @return last timestamp
* @throws DataAccessException
*/
public long getLastTimestamp() throws DataAccessException
{
// FIXME: use J2ME RecordStore here
long lastTimestamp = 0l;//this is 0L (long 0)
mailStore = PersistentStore.getPersistentObject(MAIL_LAST_TIMESTAMP_KEY);
if (mailStore.getContents() != null)
{
StringVector data = (StringVector)mailStore.getContents();
if (data.elementAt(0) != null)
{
lastTimestamp = Long.parseLong((String)data.elementAt(0));
}
}
return lastTimestamp;
}
public static void printFolderInfo(Folder f) {
StaticDataHelper.log("name: " + f.getName());
StaticDataHelper.log("fullName: " + f.getFullName());
StaticDataHelper.log("type: " + f.getType());
StaticDataHelper.log("parent: " + f.getParent());
StaticDataHelper.log("store: " + f.getStore());
StaticDataHelper.log("subfolders: " + f.list().length+"\n");
}
public static void printFolderInfo(Folder[] folders){
for (int i=0; i<folders.length; i++) {
StaticDataHelper.log("\nFolder: " + i);
printFolderInfo(folders[i]);
}
}
public static void printMessageInfo(Message msg) {
StaticDataHelper s = new StaticDataHelper();
s.log("[DEBUG]msg.folder: " + msg.getFolder().getName() +
" [" + msg.getFolder().toString() + "]");
s.log("[DEBUG]msg.folder.id: " + msg.getFolder().getId());
switch (msg.getFolder().getType()){
case Folder.OUTBOX: s.log("[DEBUG]msg.folder.type: OUTBOX"); break;
case Folder.SENT: s.log("[DEBUG]msg.folder.type: SENT"); break;
case Folder.INBOX: s.log("[DEBUG]msg.folder.type: INBOX"); break;
case Folder.DRAFT: s.log("[DEBUG]msg.folder.type: DRAFT"); break;
}
switch (msg.getStatus()){
case Message.Status.TX_SENT: s.log("[DEBUG]msg.status: TX_SENT"); break;
case Message.Status.TX_PENDING: s.log("[DEBUG]msg.status: TX_PENDING"); break;
case Message.Status.TX_SENDING: s.log("[DEBUG]msg.status: TX_SENDING"); break;
case Message.Status.TX_GENERAL_FAILURE: s.log("[DEBUG]msg.status: TX_GENERAL_FAILURE"); break;
case Message.Status.TX_ENCRYPTING: s.log("[DEBUG]msg.status: TX_ENCRYPTING"); break;
case Message.Status.TX_COMPOSING: s.log("[DEBUG]msg.status: TX_COMPOSING"); break;
case Message.Status.TX_COMPRESSING: s.log("[DEBUG]msg.status: TX_COMPRESSING"); break;
case Message.Status.TX_DELIVERED: s.log("[DEBUG]msg.status: TX_DELIVERED"); break;
case Message.Status.TX_ERROR:
s.log("[DEBUG]msg.status: TX_ERROR");
s.log("[DEBUG]msg.transMissionError: " + msg.getTransmissionError());
break;
default:
s.log("[DEBUG]msg.status:"+msg.getStatus());
}
int flags = msg.getFlags();
if ( (flags & Message.Flag.SAVED) != 0 )
s.log("[DEBUG]msg.flag: SAVED");
if ( (flags & Message.Flag.SAVED_THEN_ORPHANED) != 0 )
s.log("[DEBUG]msg.flag: SAVED_THEN_ORPHANED");
if ( (flags & Message.Flag.FILED) != 0 )
s.log("[DEBUG]msg.flag: FILED");
}
/**
* Adds the record to the folder if the record doesn't exist
*
* @param record record to store
* @return the filled record. The key may have the following values:
* valid key: okay, the record has been inserted
* null: item is silently ignored (with status 200 to the server)
* @throws DataAccessException
*/
public Record setRecord(Record record, boolean modify) throws DataAccessException {
Folder folder = null;
char type = ' '; // [I]nbox [O]utbox [D]raft [T]rash [S]ent
String locURI = record.getKey();
String mail = record.getUid();
try {
// Get mail message store for the default inistance
Store store = Session.getDefaultInstance().getStore();
if (locURI.startsWith("I/")) {
Folder[] folders = store.list(Folder.INBOX);
printFolderInfo(folders);
folder = folders[0];
type = 'I';
}
else if (locURI.startsWith("O/")) {
Folder[] folders = store.list(Folder.OUTBOX);
printFolderInfo(folders);
folder = folders[0];
type = 'O';
}
else if (locURI.startsWith("D/")) {
Folder[] folders = store.list(Folder.DRAFT);
printFolderInfo(folders);
folder = folders[0];
type ='D';
}
else if (locURI.startsWith("S/")) {
Folder[] folders = store.list(Folder.SENT);
printFolderInfo(folders);
folder = folders[0];
type = 'S';
}
else if (locURI.startsWith("T/")) {
StaticDataHelper.log("[INFO] Trash message: discard it.");
type ='T';
// invalidate the key: the record is discarded
record.setKey(null);
return record;
}
else if (locURI.startsWith("ROOT/")){
StaticDataHelper.log("[INFO] Folder info from server: not managed at the moment.");
// invalidate the key: the record is discarded
record.setKey(null);
return record;
}
else {
StaticDataHelper.log("[ERROR] Invalid folder Id from server: " + locURI);
throw new DataAccessException("Invalid folder Id from server");
}
}
catch (Exception e) {
StaticDataHelper.log("[EMAIL]Exception in MailDataStore.setRecord(:Record, :boolean): " + e.toString());
throw new DataAccessException(e);
}
// What if the server id contains another '/' ?
//String[] messageID = new StringUtil().split(locURI, "/");
String messageID = locURI.substring(2);
MessageIDStore messageIDStore = new MessageIDStore();
if (! (messageIDStore.checkIsDataThere(locURI) ||
checkIsDataThere(messageID, Constants.ITEMSKEY)) ) {
Message msg = mailParser.parseMessage(mail, locURI);
// Set the message flags depending on the message type
switch(type) {
case 'I': // Inbox
/*
* Sets the message status as an inbound (received) message:
* without using this method, the incoming message is shown
* in the Outbox instead of the Inbox
*/
msg.setInbound(true);
/*
* Indicates that the message has been received successfully
*/
msg.setStatus(Message.Status.RX_RECEIVED, 1);
/*
* This lets the user to answer to the message
*/
msg.setFlag(Message.Flag.REPLY_ALLOWED, true);
break;
case 'S': // Sent
msg.setStatus(Message.Status.TX_SENT, 1);
break;
case 'D': // Draft
msg.setFlag(Message.Flag.SAVED_THEN_ORPHANED, true);
break;
case 'T': // Trash
// FIXME: is it possible to store a message in the trash?
// Shall we just silently discard this?
break;
}
// Save the message to the correct folder
folder.appendMessage(msg);
// XXX: why this conversion?
Integer msgId = new Integer(msg.getMessageId());
String key = new String(type + "/" + msgId.toString());
// Set record key
record.setKey(key);
// Add GUID/LUID mapping
messageIDStore.add(locURI, msgId.toString());
// Append msg id to this DataStore
appendData(type + ":" + msgId.toString(), Constants.ITEMSKEY);
}
return record;
}
/**
* Delete a record from the folder.
* @param record
* @throws DataAccessException
*/
public void deleteRecord(Record record) throws DataAccessException {
Folder folder = null;
String locURI = record.getKey();
String[] serverMessageID = new StringUtil().split(locURI,"/");
MessageIDStore messageIDStore = new MessageIDStore();
int messageID;
String messageIDfromRS = messageIDStore.getClientId(locURI);
if (messageIDfromRS == null) {
messageID = Integer.parseInt(serverMessageID[1]);
} else {
messageID = Integer.parseInt(messageIDfromRS);
messageIDStore.delete(locURI);
}
Message todelete = Store.getMessage(messageID);
if (todelete != null) {
folder = todelete.getFolder();
folder.deleteMessage(todelete);
deleteData(new Integer(todelete.getMessageId()).toString(),Constants.ITEMSKEY);
} else {
try {
Store store = Session.getDefaultInstance().getStore();
Folder[] folders = store.list();
for (int index = 0; index < folders.length; index++) {
Folder f = folders[index];
Message[] mailMsg = f.getMessages();
if (mailMsg != null) {
for (int i=0; i < mailMsg.length; i++) {
if ( mailMsg[i].getMessageId() == messageID) {
f.deleteMessage(mailMsg[i]);
}
}
}
}
} catch (Exception e) {
StaticDataHelper.log(e.getMessage());
}
}
folder = null;
}
/**
* Delete data from store
*
* @param String: data
* @param long: key
*/
public void deleteData(String data, long key) {
persist = PersistentStore.getPersistentObject(key);
String[] vals = (String[])persist.getContents();
if (vals == null) {
return;
}
else {
boolean flag = false;
int j = 0;
String[] small = new String[vals.length];
for (int i=0; i < vals.length; i++) {
String[] compare = new StringUtil().split(vals[i], ":");
if (compare != null && !compare[1].equals(data)) {
small[j++]=vals[i];
} else {
flag = true;
}
}
if (flag) {
if (j > 0) {
String result[] = new String[j];
for (int k=0;k<j;k++) {
result[k] = small[k];
}
persist.setContents(result);
persist.commit();
} else {
PersistentStore.destroyPersistentObject(Constants.ITEMSKEY);
}
}
else {
persist.setContents(small);
persist.commit();
}
}
}
/**
* return no deleted records from device recordstore
*
* @return find records
* @throws DataAccessException
*/
public Vector getNoDeletedRecords() throws DataAccessException {
return null;
}
/**
* return no deleted records from device recordstore
*
* @return find records
* @throws DataAccessException
*/
public boolean getNextRecords(Vector v) throws DataAccessException {
boolean moreElements = true;
String message = null;
Record record = null;
try {
for (int i=0; i<page; ++i) {
moreElements = items.hasMoreElements();
if (!moreElements) {
break;
}
v.addElement((Record)items.nextElement());
}
return moreElements;
} catch (Exception e) {
StaticDataHelper.log(e.getMessage());
throw new DataAccessException(e);
}
}
/**
* FIXME: This method is like getNextRecord but used for fast sync.
* Could be just one method with a parameter (to be checked
* in all calling points)
*
* @param dummy Not used
*
* @return {@code true} if {@code <Final/>} command from
* server not reached (there are more elements to be
* processed)
*
* @throws DataAccessException
*/
public boolean getNextRecords(Vector v, char dummy) throws DataAccessException {
boolean moreElements = true;
String message = null;
Record record = null;
try {
for (int i = 0; i < page; ++i) {
moreElements = items.hasMoreElements();
if (!moreElements) {
break;
}
v.addElement((Record)items.nextElement());
}
return moreElements;
} catch(Exception e) {
StaticDataHelper.log(e.getMessage());
throw new DataAccessException(e);
}
}
/**
* execute init recordstore operations
*/
public void startDSOperations() {
try {
messageVector = new Vector();
processAllDeletedMessages(Integer.parseInt(alertCode));
getMessagesFromFolders(Integer.parseInt(alertCode));
items = messageVector.elements();
} catch (Exception e) {
StaticDataHelper.log("[EMAIL]Exception in MailDataStore.startDSOperations(): " + e.toString());
}
}
/**
* Appends all the deleted messages to the vector.
* For Slow Sync, just remove the events from the list.
*/
private void processAllDeletedMessages(int alertCode) {
persist = PersistentStore.getPersistentObject(Constants.DELETEDITEMS);
String[] deletedMessages = (String[])persist.getContents();
if (deletedMessages == null) {
return;
}
for (int deletedIndex = 0; deletedIndex < deletedMessages.length; deletedIndex++) {
String[] message = new StringUtil().split(deletedMessages[deletedIndex], ":");
if (alertCode != ALERT_CODE_SLOW) {
MessageIDStore messageIDStore = new MessageIDStore();
if (!messageIDStore.checkIsDataThere(message[1])) {
continue;
}
StringBuffer messageBuffer = new StringBuffer();
messageBuffer.append("<Item>"+"\n");
messageBuffer.append("<Source>"+"\n");
messageBuffer.append("<LocURI>");
String messageIDfromStore = messageIDStore.getServerId(message[1]);
if (messageIDfromStore != null) {
String[] id = new StringUtil().split(messageIDfromStore,"/");
if (id != null) {
message[0] = id[0];
}
messageIDStore.delete(messageIDfromStore);
}
messageIDfromStore = message[0] + "/" + message[1];
messageBuffer.append(messageIDfromStore);
messageBuffer.append("</LocURI>"+"\n");
messageBuffer.append("</Source>"+"\n");
messageBuffer.append("</Item>"+"\n");
StaticDataHelper.log("[LOG]In MailDataStore.processAllDeletedMessages() --> messageBuffer: " + messageBuffer.toString());
Record rec = new Record(messageIDfromStore,
RECORD_STATE_DELETED,
messageBuffer.toString());
messageVector.addElement(rec);
messageBuffer = null;
}
deleteData(message[1], Constants.ITEMSKEY);
}
PersistentStore.destroyPersistentObject(Constants.DELETEDITEMS);
}
/**
* Depending on the passed alert code gets messages from mail folders
*
* @param alertCode The integer value coming from the {@code <Alert>}
* @throws Exception
*/
private void getMessagesFromFolders(int alertCode) throws Exception {
/*
* the RIM Session class provides
* access to email services. The
* Store instance is used to access
* message storage on this device
*/
Store store = Session.getDefaultInstance().getStore();
Folder[] inbox = store.list(Folder.INBOX);
Folder[] outbox = store.list(Folder.OUTBOX);
Folder[] sent = store.list(Folder.SENT);
Folder[] draft = store.list(Folder.DRAFT);
// Process Inbox
getMessagesFromFolder(inbox[0], "I", alertCode);
// Process Outbox
getMessagesFromFolder(outbox[0], "O", alertCode);
// Process Sent, if different from Outbox
if( !sent[0].equals(outbox[0]) ) {
getMessagesFromFolder(sent[0], "O", alertCode);
}
// Process Draft, if different from Outbox
if( !draft.equals(outbox) ) {
getMessagesFromFolder(draft[0], "D", alertCode);
}
}
/**
* Based on alertCode get messages from the specified mail folder.
* @param int: alertCode
* @throws Exception
*/
private void getMessagesFromFolder(Folder f, String folderType, int alertCode) throws Exception {
Message[] mailMsg = f.getMessages();
for (int msgIndex = 0; msgIndex < mailMsg.length;msgIndex++) {
MessageIDStore messageIDStore = new MessageIDStore();
String ftype = folderType;
StaticDataHelper.log(mailMsg[msgIndex].getSubject());
printMessageInfo(mailMsg[msgIndex]);
if (mailMsg[msgIndex].isSet(Message.Flag.SAVED_THEN_ORPHANED)){
ftype = "D";
}
else if(mailMsg[msgIndex].getStatus() == Message.Status.TX_SENT){
ftype = "S";
}
if (ALERT_CODE_FAST == alertCode) {
doFastSync(ftype, mailMsg[msgIndex]);
}
else {
doSlowSync(ftype, mailMsg[msgIndex]);
}
}
}
/**
* generates SyncML for fast sync
* @param String :folderName name of folder
* @param Message :mailMsg mail message to be synced
* @return SyncML string for the message, corresponding to type of sync and message
*/
private String doFastSync(String folderName, Message mailMsg){
StringBuffer buffer = new StringBuffer();
if (!checkIsDataThere((new Integer(mailMsg.getMessageId()).toString()), Constants.ITEMSKEY )) {
mailParser.setParseParams(folderName,mailMsg);
buffer.append(mailParser.toString());
appendData(folderName + ":" + new Integer(mailMsg.getMessageId()).toString(), Constants.ITEMSKEY);
String messageIDfromStore = new MessageIDStore().getServerId(new Integer(mailMsg.getMessageId()).toString());
if (messageIDfromStore == null) {
messageIDfromStore = folderName + "/" + mailMsg.getMessageId();
//new MessageIDStore().add(
}
Record rec = new Record(messageIDfromStore, RECORD_STATE_NEW, buffer.toString());
messageVector.addElement(rec);
}
return buffer.toString();
}
/**
* generates SyncML for slow sync
* @param String :folderName name of folder.
* @param Message :mailMsg mail message to be synced
* @return SyncML string for the message, corresponding to type of sync and message
*/
private String doSlowSync(String folderName,Message mailMsg) {
StringBuffer buffer = new StringBuffer();
mailParser.setParseParams(folderName, mailMsg);
buffer.append(mailParser.toString());
String messageIDfromStore = new MessageIDStore().getServerId(new Integer(mailMsg.getMessageId()).toString());
if (messageIDfromStore == null) {
messageIDfromStore = folderName + "/" + mailMsg.getMessageId();
}
Record rec = new Record(messageIDfromStore, RECORD_STATE_NEW, buffer.toString());
messageVector.addElement(rec);
if (!checkIsDataThere((new Integer(mailMsg.getMessageId()).toString()), Constants.ITEMSKEY )) {
appendData(folderName + ":" + new Integer(mailMsg.getMessageId()).toString(), Constants.ITEMSKEY);
}
return buffer.toString();
}
/**
* FIXME: not used for email
*
* execute commit recordstore operations
* remove records signed as DELETED 'D'
* mark UNSIGNED ' ' records signed as NEW 'N' and UPDATED 'U'
*
* @throws DataAccessException
*
*/
public void commitDSOperations() throws DataAccessException {
StaticDataHelper.log("[DEBUG]Entered in MailDataStore.commitDSOperations()");
}
/**
* reset modifiedItems cursor
*/
public void resetModificationCursor() {
if (modifiedItems != null)
modifiedItems.reset();
}
public long getNextKey() {
return 0;
}
/**
* Finds out if data is present in the store
*
* @param data The message id of the message to be searched
* @param key The key for this store
* @return 'true' if data is present, else 'false'
*/
public boolean checkIsDataThere(String data, long key) {
persist = PersistentStore.getPersistentObject(key);
String[] vals = (String[])persist.getContents();
if (vals != null) {
for (int i = 0; i < vals.length; i++) {
String[] compare = new StringUtil().split(vals[i], ":");
if (compare[1].equals(data)) {
StaticDataHelper.log("[LOG]In MailDataStore.checkIsDataThere() 'data' is " + data);
return true;
}
}
}
return false;
}
/**
* Adds new data to persitent store
* @param data The string to be added (e.g. "O:45854695")
* @param key The key for this store
*/
public void appendData(String data, long key) {
StaticDataHelper.log("[LOG]In MailDataStore.appendData(:String, :long)");
persist = PersistentStore.getPersistentObject(key);
String[] vals = (String[])persist.getContents();
if (vals == null) {
vals = new String[1];
vals[0] = new String(data);
persist.setContents(vals);
persist.commit();
return;
} else {//FIXME: implement a better way to copy the array...
String[] large = new String[vals.length + 1];
int i = 0;
for (i = 0; i < vals.length; i++) {
large[i] = vals[i];
}
large[i] = data;
persist.setContents(large);
persist.commit();
}
}
}