/* * Funambol Citadel Connector * (C) 2007-2008 Mathew McBride * http://bionicmessage.net * * Portions of code may come from: * 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 net.bionicmessage.funambol.citadel.sync; import com.funambol.email.engine.source.HelperForFilter; import com.funambol.email.exception.EntityException; import com.funambol.email.model.EmailFilter; import com.funambol.email.pdi.converter.FolderToXML; import com.funambol.email.pdi.folder.Folder; import com.funambol.email.pdi.parser.XMLFolderParser; import com.funambol.email.util.Def; import com.funambol.framework.core.AlertCode; import com.funambol.framework.engine.SyncItem; import com.funambol.framework.engine.SyncItemImpl; import com.funambol.framework.engine.SyncItemKey; import com.funambol.framework.engine.SyncItemState; import com.funambol.framework.engine.source.AbstractSyncSource; import com.funambol.framework.engine.source.FilterableSyncSource; import com.funambol.framework.engine.source.SyncContext; import com.funambol.framework.engine.source.SyncSourceException; import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.sql.Timestamp; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Properties; import java.util.logging.FileHandler; import java.util.logging.Level; import java.util.logging.Logger; import net.bionicmessage.funambol.citadel.store.CitadelMailObject; import net.bionicmessage.funambol.citadel.store.CtdlFnblConstants; import net.bionicmessage.funambol.citadel.store.EmailObjectStore; import net.bionicmessage.funambol.citadel.util.HTMLFormatter; import net.bionicmessage.funambol.util.Lookup; /** * * @author matt */ public class CitadelSyncSource extends AbstractSyncSource implements FilterableSyncSource { protected SyncContext ctx = null; protected EmailObjectStore eos = null; protected Properties syncSourceProperties = null; protected Logger log = null; protected String storeLoc = null; protected FileHandler fh = null; public static final SyncItemKey inboxKey = new SyncItemKey("ROOT/I"); public static final SyncItemKey sentKey = new SyncItemKey("ROOT/S"); public static final SyncItemKey trashKey = new SyncItemKey("ROOT/T"); public static final SyncItemKey outboxKey = new SyncItemKey("ROOT/O"); protected EmailFilter filter = null; protected String userStoreLoc = null; private SyncItemKey[] updatedSyncItemKeys = null; private boolean startedSync = false; public void CitadelSyncSource() { } /** * Called before any other synchronization method. To interrupt the sync * process, throw a SyncSourceException. * * @param syncContext the context of the sync. * * @see SyncContext * * @throws SyncSourceException to interrupt the process with an error */ public void beginSync(SyncContext syncContext) throws SyncSourceException { log = Logger.getLogger(CtdlFnblConstants.SYNC_SOURCE_LOGGER + "-" + System.currentTimeMillis()); this.ctx = syncContext; storeLoc = syncSourceProperties.getProperty(CtdlFnblConstants.STORE_LOC); //todo: escape userStoreLoc = storeLoc + File.separatorChar + ctx.getPrincipal().getDeviceId()+"_"+ctx.getPrincipal().getUsername(); File storeDir = new File(userStoreLoc); if (!storeDir.exists()) { storeDir.mkdirs(); } try { setupLogging(); log.info("beginSync(" + ctx.getPrincipal().getName() + ")"); } catch (IOException ex) { throw new SyncSourceException("Cannot set up logging", ex); } Properties storeProps = new Properties(syncSourceProperties); storeProps.setProperty(CtdlFnblConstants.STORE_LOC, userStoreLoc); if (ctx.getSyncMode() == AlertCode.SLOW) { log.info("Slow sync mode. Purging old data"); storeProps.setProperty(CtdlFnblConstants.PURGE_DB_OPTION, ""); } eos = new EmailObjectStore(storeProps); eos.setCredentials(ctx.getPrincipal().getUsername(), ctx.getPrincipal().getUser().getPassword()); if (ctx.getFilterClause() != null && ctx.getFilterClause().getClause() != null) { try { filter = HelperForFilter.setFilter(ctx, 1, 2, true, true, false, false, false); } catch (EntityException ex) { log.log(Level.SEVERE, "Error setting up filter", ex); } } try { long fromTime = 0; if (filter != null && filter.getTime() != null) fromTime = filter.getTime().getTime(); eos.startSync(fromTime); startedSync = true; } catch (Exception ex) { log.log(Level.SEVERE, "Failure in server sync", ex); throw new SyncSourceException("Error in server sync", ex); } updatedSyncItemKeys = null; } /** * Called after the modifications have been applied. * * @throws SyncSourceException to interrupt the process with an error */ public void endSync() throws SyncSourceException { log.info("endSync()"); try { if (eos != null && startedSync) { eos.close(); } else { log.info("Warning: email object store is null or sync never started"); } fh.close(); } catch (Exception ex) { throw new SyncSourceException("Error closing database", ex); } } /** * Commits the changes applied during the sync session. If the underlying * datastore can not commit the changes, a SyncSourceException is thrown. * * @throws SyncSourceException if the changes cannot be committed */ public void commitSync() throws SyncSourceException { log.info("commitSync()"); super.commitSync(); } /** * Called to get the keys of the items updated in the time frame sinceTs - untilTs. * <br><code>sinceTs</code> null means all keys of the items updated until <code>untilTs</code>. * <br><code>untilTs</code> null means all keys of the items updated since <code>sinceTs</code>. * * @param sinceTs consider the changes since this point in time. * @param untilTs consider the changes until this point in time. * * @return an array of keys containing the <code>SyncItemKey</code>'s key of the updated * items in the given time frame. It MUST NOT return null for * no keys, but instad an empty array. */ public SyncItemKey[] getUpdatedSyncItemKeys(Timestamp sinceTs, Timestamp untilTs) throws SyncSourceException { log.info("getUpdatedSyncItemKeys()"); List<CitadelMailObject> seenStatus = eos.getSeenStatusUpdated(); updatedSyncItemKeys = new SyncItemKey[seenStatus.size()]; for (int i = 0; i < seenStatus.size(); i++) { CitadelMailObject cmo = seenStatus.get(i); String key = "I/" + Long.toString(cmo.getCtdlMessagePointer()); updatedSyncItemKeys[i] = new SyncItemKey(key); log.fine("Updated " + cmo.getCtdlMessagePointer()); } return updatedSyncItemKeys; } /** * Called to get the keys of the items deleted in the time frame sinceTs - untilTs. * <br><code>sinceTs</code> null means all keys of the items deleted until <code>untilTs</code>. * <br><code>untilTs</code> null means all keys of the items deleted since <code>sinceTs</code>. * * @param sinceTs consider the changes since this point in time. * @param untilTs consider the changes until this point in time. * * @return an array of keys containing the <code>SyncItemKey</code>'s key of the deleted * items in the given time frame. It MUST NOT return null for * no keys, but instad an empty array. */ public SyncItemKey[] getDeletedSyncItemKeys(Timestamp sinceTs, Timestamp untilTs) throws SyncSourceException { log.info("getDeletedSyncItemKeys()"); List<CitadelMailObject> deletedObjects = eos.getDeletedOnServerObjects(); SyncItemKey[] deletedKeys = new SyncItemKey[deletedObjects.size()]; int i = 0; Iterator<CitadelMailObject> deletedIterator = deletedObjects.iterator(); while (deletedIterator.hasNext()) { CitadelMailObject deleted = deletedIterator.next(); SyncItemKey dKey = new SyncItemKey( Long.toString(deleted.getCtdlMessagePointer())); deletedKeys[i++] = dKey; } return deletedKeys; } /** * Called to get the keys of the items created in the time frame sinceTs - untilTs. * <br><code>sinceTs</code> null means all keys of the items created until <code>untilTs</code>. * <br><code>untilTs</code> null means all keys of the items created since <code>sinceTs</code>. * * @param sinceTs consider the changes since this point in time. * @param untilTs consider the changes until this point in time. * * @return an array of keys containing the <code>SyncItemKey</code>'s key of the created * items in the given time frame. It MUST NOT return null for * no keys, but instad an empty array. */ public SyncItemKey[] getNewSyncItemKeys(Timestamp sinceTs, Timestamp untilTs) throws SyncSourceException { log.info("getNewSyncItemKeys()"); List<CitadelMailObject> newOnServer = eos.getAddedOnServerObjects(); int newSize = newOnServer.size(); SyncItemKey[] newKeys = new SyncItemKey[newSize]; Iterator<CitadelMailObject> it = newOnServer.iterator(); int i = 0; while (it.hasNext()) { CitadelMailObject newMail = it.next(); String msgPointerF = "I/" + Long.toString(newMail.getCtdlMessagePointer()); SyncItemKey newKey = new SyncItemKey(msgPointerF); newKeys[i++] = newKey; } return newKeys; } /** * Adds a new <code>SyncItem</code>. * The item is also returned giving the opportunity to the * source to modify its content and return the updated item (i.e. updating * the id to the GUID). * * @param syncInstance the item to add * * @return the inserted item * * @throws SyncSourceException in case of error (for instance if the * underlying data store runs into problems) */ public SyncItem addSyncItem(SyncItem syncInstance) throws SyncSourceException { try { log.info("addSyncItem(" + syncInstance.getKey().getKeyAsString() + ")"); InboundEmailItem iei = new InboundEmailItem(); iei.setSourceItem(syncInstance); // What room does it belong to? char prefix = determinePrefix(syncInstance.getKey()); String room = syncSourceProperties.getProperty(CtdlFnblConstants.ROOM_MAIL); if (prefix != 'I' && prefix == 'S') { room = syncSourceProperties.getProperty(CtdlFnblConstants.ROOM_SENT); } String toString = iei.getToAddresses(); String ccString = iei.getCCAddresses(); String bccString = iei.getBCCAddresses(); String subject = iei.getSubject(); String fromName = iei.getSenderPersonName(); String fromMail = iei.getSenderAddress(); String rfc822 = iei.getRFC822Content(); // Pray to $DEITY it works eos.getToolkit().postMessage(room, toString, subject, fromName, ccString, bccString, fromMail, rfc822); return iei.generateAckSyncItem(this); } catch (Exception ex) { log.throwing(this.getClass().getName(), "addSyncItem", ex); throw new SyncSourceException(ex); } } /** * Update a <code>SyncItem</code>. * The item is also returned giving the opportunity to the * source to modify its content and return the updated item (i.e. updating * the id to the GUID). * * @param syncInstance the item to replace * * @return the updated item * * @throws SyncSourceException in case of error (for instance if the * underlying data store runs into problems) */ public SyncItem updateSyncItem(SyncItem syncInstance) throws SyncSourceException { log.info("updateSyncItem(" + syncInstance.getKey().getKeyAsString() + ")"); try { InboundEmailItem inboundEmailItem = new InboundEmailItem(); inboundEmailItem.setSourceItem(syncInstance); boolean isRead = inboundEmailItem.isRead(); SyncItemKey originalKey = removePrefix(syncInstance.getKey()); CitadelMailObject cmo = eos.getMessageByPointer(originalKey.getKeyAsString()); if (cmo.isSeen() != isRead) { // Set message flag cmo.setSeen(isRead); eos.getToolkit().setSeenStatus(originalKey.getKeyAsString(), isRead); } } catch (Exception ex) { log.log(Level.SEVERE, null, ex); } return getSyncItemFromId(syncInstance.getKey()); // don't do anything, just yet. } /** * Removes a SyncItem given its key. * * @param itemKey the key of the item to remove * @param time the time of the deletion * @param softDelete is a soft delete ? * * @throws SyncSourceException in case of error (for instance if the * underlying data store runs into problems) */ public void removeSyncItem(SyncItemKey itemKey, Timestamp time, boolean softDelete) throws SyncSourceException { log.info("removeSyncItem(" + itemKey.getKeyAsString() + ")"); if (!softDelete) { try { SyncItemKey withoutFolder = removePrefix(itemKey); CitadelMailObject cmo = eos.getMessageByPointer(withoutFolder.getKeyAsString()); //eos.getToolkit().deleteMessage(withoutFolder.getKeyAsString()); eos.moveToTrash(withoutFolder.getKeyAsString()); } catch (Exception ex) { log.log(Level.SEVERE, "Exception while deleting message", ex); throw new SyncSourceException(ex); } } } /** * Called to get the keys of all items accordingly with the parameters * used in the beginSync call. * @return an array of all <code>SyncItemKey</code>s stored in this source. * If there are no items an empty array is returned. * * @throws SyncSourceException in case of error (for instance if the * underlying data store runs into problems) */ public SyncItemKey[] getAllSyncItemKeys() throws SyncSourceException { log.info("getAllSyncItemKeys()"); ArrayList keysToOutput = new ArrayList(); Iterator<String> mailIterator = null; /* Iterator<String> pointerIterator = allObjs.iterator(); while (pointerIterator.hasNext()) { String msgPointer = pointerIterator.next(); allKeys[i++] = new SyncItemKey(msgPointer); } */ long filterTime = 0; if (filter != null && filter.getTime() != null) { filterTime = filter.getTime().getTime(); mailIterator = eos.listMessagesInRoomFromTime("Mail", filterTime).iterator(); } else { mailIterator = eos.listMessagesInRoom("Mail").iterator(); } while (mailIterator.hasNext()) { boolean allow = true; String msgPointer = mailIterator.next(); String key = "I/" + msgPointer; CitadelMailObject cmo = eos.getMessageByPointer(msgPointer); Date mdate = cmo.getTime(); // Do filter check if (filter != null && filter.getTime() != null && mdate.getTime() > filter.getTime().getTime()) { keysToOutput.add(new SyncItemKey(key)); } else if (filter == null || filter.getTime() == null) { keysToOutput.add(new SyncItemKey(key)); } } keysToOutput.add(inboxKey); keysToOutput.add(outboxKey); SyncItemKey[] allKeys = new SyncItemKey[keysToOutput.size()]; keysToOutput.toArray(allKeys); return allKeys; } /** * Called to get the item with the given key. * * @return return the <code>SyncItem</code> corresponding to the given * key. If no item is found, null is returned. * * @param syncItemKey the key of the SyncItem to return * * @throws SyncSourceException in case of errors (for instance if the * underlying data store runs into problems) */ public SyncItem getSyncItemFromId(SyncItemKey syncItemKey) throws SyncSourceException { log.info("getSyncItemFromId(" + syncItemKey.getKeyAsString() + ")"); if (syncItemKey.getKeyAsString().startsWith("ROOT/")) { return getSyncItemForFolder(syncItemKey); } SyncItemKey actual = removePrefix(syncItemKey); log.info("Actual sync item key: " + actual.getKeyAsString()); boolean isAnUpdate = Lookup.isInArray(updatedSyncItemKeys, syncItemKey); CitadelMailObject cmo = null; OutboundEmailItem omi = new OutboundEmailItem(); if (!isAnUpdate) { try { cmo = eos.getFilledMessageByPointer(actual.getKeyAsString()); if (cmo == null) { return new SyncItemImpl(this, syncItemKey, SyncItemState.DELETED); } } catch (Exception ex) { Logger.getLogger(CitadelSyncSource.class.getName()).log(Level.SEVERE, null, ex); throw new SyncSourceException("Error getting message: " + actual.getKeyAsString(), ex); } omi.setMailObject(cmo); if (filter != null) { omi.setCropAtBytes(filter.getNumBytes()); } try { if (filter != null && filter.getSize() == 63 && omi.doAttachmentsFitInsideCrop()) { // Does it fit? Attach the parts eos.fillPartsForMessage(cmo); omi.setAddAttachments(true); } } catch (Exception e) { log.log(Level.WARNING, "Unable to add attachments", e); } } else { cmo = eos.getMessageByPointer(actual.getKeyAsString()); omi.setMailObject(cmo); } try { SyncItemKey parent = null; if (cmo.getCtdlMessageRoom().equals("Mail")) { parent = inboxKey; } else { parent = sentKey; } SyncItem si = omi.getSyncItem(this, actual, parent); if (!isAnUpdate) { si.setState(SyncItemState.SYNCHRONIZED); } else { si.setState(SyncItemState.UPDATED); } return si; } catch (Exception e) { log.throwing(this.getClass().getName(), "getSyncItemFromId", e); throw new SyncSourceException("Error converting message: " + syncItemKey.getKeyAsString(), e); } } /** * Called to retrive the keys of the twins of the given item * * @param syncItem the twin item * * @return the keys of the twin. Each source implementation is free to * interpret this as it likes (i.e.: comparing all fields). * * * @throws SyncSourceException in case of errors (for instance if the * underlying data store runs into problems) */ public SyncItemKey[] getSyncItemKeysFromTwin(SyncItem syncItem) throws SyncSourceException { log.info("getSyncItemKeysFromTwin(" + syncItem.getKey().getKeyAsString() + ")"); if (syncItem.getType().equals("application/vnd.omads-folder+xml")) { try { ByteArrayInputStream bis = new ByteArrayInputStream(syncItem.getContent()); XMLFolderParser xfp = new XMLFolderParser(bis); Folder f = xfp.parse(); if (f.getRole().getPropertyValueAsString().equals(Def.FOLDER_INBOX_ROLE)) { return new SyncItemKey[]{inboxKey}; } else if (f.getRole().getPropertyValueAsString().equals(Def.FOLDER_OUTBOX_ROLE)) { return new SyncItemKey[]{outboxKey}; } } catch (Exception e) { log.log(Level.WARNING, "Error getting twin for folder", e); } } return null; } /** * Called by the engine to notify an operation status. * @param operationName the name of the operation. * One between: * - Add * - Replace * - Delete * @param status the status of the operation * @param keys the keys of the items */ public void setOperationStatus(String operationName, int status, SyncItemKey[] keys) { StringBuffer logOutput = new StringBuffer(); for (int i = 0; i < keys.length; i++) { SyncItemKey syncItemKey = keys[i]; logOutput.append("setOperationStatus(").append(operationName).append(","); logOutput.append(status).append(",").append(keys[i].getKeyAsString()); logOutput.append(")\n"); } log.info(logOutput.toString()); } public void setSyncSourceProperties(Properties syncSourceProperties) { this.syncSourceProperties = syncSourceProperties; } public Properties getSyncSourceProperties() { return syncSourceProperties; } protected void setupLogging() throws IOException { String connectorLog = null; String suffix = ""; if (syncSourceProperties.getProperty(CtdlFnblConstants.CONNECTOR_SINGLE_LOG_OPTION) == null) { suffix = "-" + System.currentTimeMillis(); } connectorLog = String.format("%s%s%s%s.html", userStoreLoc, File.separator, "funambol", suffix); File logFile = new File(connectorLog); fh = new FileHandler(connectorLog); fh.setFormatter(new HTMLFormatter()); fh.setLevel(Level.ALL); log.setLevel(Level.ALL); log.addHandler(fh); } public String getType() { return ""; } public SyncItem getSyncItemForFolder(SyncItemKey folder) throws SyncSourceException { try { Folder fl = new Folder(); fl.getUID().setPropertyValue(folder.getKeyAsString()); if (folder.getKeyAsString().endsWith("I")) { fl.getName().setPropertyValue(Def.FOLDER_INBOX_ENG); fl.getRole().setPropertyValue(Def.FOLDER_INBOX_ROLE); } else if (folder.getKeyAsString().endsWith("S")) { fl.getName().setPropertyValue(Def.FOLDER_SENT_ENG); fl.getRole().setPropertyValue(Def.FOLDER_SENT_ROLE); } else if (folder.getKeyAsString().endsWith("O")) { fl.getName().setPropertyValue(Def.FOLDER_OUTBOX_ENG); fl.getRole().setPropertyValue(Def.FOLDER_OUTBOX_ROLE); } fl.getParentId().setPropertyValue("ROOT"); FolderToXML ftxml = new FolderToXML(null, null); String content = ftxml.convert(fl); SyncItemImpl si = new SyncItemImpl(this, folder); si.setContent(content.getBytes("UTF-8")); si.setType("application/vnd.omads-folder+xml"); return si; } catch (Exception ex) { log.throwing(this.getClass().getName(), "getSyncItemFromFolder", ex); throw new SyncSourceException(ex); } } /** Remove a prefix (I/, S/ etc.) */ private SyncItemKey removePrefix(SyncItemKey fullKey) { String fullString = fullKey.getKeyAsString(); if (fullString.contains("/")) { String[] split = fullString.split("/"); fullString = split[1]; } fullKey.setKeyValue(fullString); return fullKey; } private char determinePrefix(SyncItemKey fullKey) { String fullString = fullKey.getKeyAsString(); if (fullString.substring(0, 3).contains("/")) { return fullString.charAt(0); } else { return 'I'; } } public char getSyncItemStateFromId(SyncItemKey arg0) throws SyncSourceException { log.log(Level.FINER, "getSyncItemStateFromId(" + arg0.getKeyAsString() + ")"); if (arg0.getKeyAsString().contains("O/")) { return SyncItemState.DELETED; } return SyncItemState.SYNCHRONIZED; } public boolean isSyncItemInFilterClause(SyncItem arg0) throws SyncSourceException { if (arg0 != null) { log.log(Level.FINER, "isSyncItemInFilterClause(" + arg0.getKey().getKeyAsString() + ")"); return true; } return false; } public boolean isSyncItemInFilterClause(SyncItemKey arg0) throws SyncSourceException { if (arg0 != null) { log.log(Level.FINER, "isSyncItemInFilterClause(" + arg0.getKeyAsString() + ")"); return true; } return false; } }