Initial import of ActiveSync support into master branch.
authorMichael J. Rubinsky <mrubinsk@horde.org>
Fri, 26 Mar 2010 18:06:41 +0000 (14:06 -0400)
committerMichael J. Rubinsky <mrubinsk@horde.org>
Fri, 26 Mar 2010 18:37:46 +0000 (14:37 -0400)
This should go without saying, but:

This code is very experimental at the moment. It is functional in my tests,
but there is sure to be many bugs still to be worked out. If you use activesync
support at this point, it may work, it may not, it may make your iPod grow legs
and run for high ground. You have been warned ;)

Will put up some documentation, TODOs, issues etc... as I have time  on the wiki project page at

http://wiki.horde.org/Project/ActiveSync

Squashed commit of the following:

commit d8d6dafb925503e328b3688cec6422ab130bd77c
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Fri Mar 26 13:45:47 2010 -0400

    remove commented out code

commit f790aa31c91e6c0747dc2b50e26c59755ec71cf4
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Fri Mar 26 13:44:25 2010 -0400

    add a bare-bones config tab for activesync.

    Right now, it just enables/disables as sets the directory for the state files

commit aef0cf5654ea5b0daa75202fcae0edee95701221
Merge: 54bee83 8e71015
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Fri Mar 26 13:16:24 2010 -0400

    Merge branch 'master' into ActiveSync-refactor1

commit 54bee830a1df9bfa219e642a8492377a8507295c
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Fri Mar 26 13:13:05 2010 -0400

    Eh, turns out that SYNC_POOMCAL_EXCEPTIONSTARTTIME is actually supposed to hold the
    *original* start time of the recurring event, not the exception's start time. THAT goes
    in the SYNC_POOMCAL_STARTTIME field, just like a normal appointment.

    Thank you, MS.

commit 2dc95253f0fb2dbaa859c9dba4230df441cf9233
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Fri Mar 26 13:11:08 2010 -0400

    Changes to fix timezone handling again.

    All dates are stored intenrally by the message object as Horde_Date objects
    in the local tz. The encoder will convert to UTC before putting it out to the pim.
    Likewise, the decoder creates Horde_Date objects in the local tz from the UTC time
    passed to us from the PIM.

commit 4866f716d446f10bb93722a016f9bf9c6c102e2d
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Fri Mar 26 13:09:40 2010 -0400

    wrap

commit 2460101269881807de4265eae1330dea992e8357
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Fri Mar 26 13:08:35 2010 -0400

    return the baseid as well

commit 8ecc8836c4282a3fc5779a53661e21ba745d596e
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Fri Mar 26 10:53:54 2010 -0400

    Start making the activesync message objects more like a content type mapper.

    Start providing a similar api for setting/getting properties from these types of objects.
    This commit completely rewrites the Appointment type, along with the supporting code
    in kronolith to convert an event from/to an activesync appointment.

    Still some issues with recurrence exceptions to work out.

commit c6a871cc3e00b2e8f12b64e90fed53728d73580b
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Fri Mar 26 10:51:48 2010 -0400

    Add wbxml mapping for POOMCAL:ResponseType

commit 1f742938b6b0049985970fb26d23a2faeffa33b4
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Fri Mar 26 10:50:10 2010 -0400

    give this a default value

commit dec6bf7e24ab03603bfb50a96ecd8186efd0ff7b
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Thu Mar 25 12:37:32 2010 -0400

    Allow filtering out events that represent exceptions to recurring events.

commit 7a0ba71f1a48016516912015bce3c36e72448ff7
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Thu Mar 25 11:07:57 2010 -0400

    Add a baseid property to events.

    Used for mapping an event that represents an exception to the
    original, recurring event. Needed for dealing with exceptions when
    syncing with ActiveSync devices (and possibly others).

commit 9585333db3e219047790929f446048821a3695a2
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Wed Mar 24 20:41:00 2010 -0400

    Fix default value for image_faces value in ansel_images table

commit dca8d32e8925e5e5afdead1bb1e54e9d2107853d
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Thu Mar 18 18:44:06 2010 -0400

    Fix PROVISION command handling

commit f11c04a5a25e9b221c65c06a2e039f2e3f8626c3
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Wed Mar 17 18:01:50 2010 -0400

    remove commented out, obsolete code

commit 0685902176b2bdf3d719ae2bc4ec0249f136d6a6
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Wed Mar 17 12:47:53 2010 -0400

    Remove obsolete files

commit 8d099d9d52178da2cc38f939a4cfa27d69711c76
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Wed Mar 17 12:46:16 2010 -0400

    Add Horde_ActiveSync_Message objects, remove Horde_ActiveSync_Streamer_* objects.

    Also, phpdoc, add encodeStream() and decodeStream() to allow encode() and decode()
    to take strings.

commit f5bda8a80b6603ca6b044c3a2cee6d0454fdbcce
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Mon Mar 15 15:58:15 2010 -0400

    Add mostly un-implemented methods for working with a history-based state driver

commit b1a2ecb358c8e314b7d50e9491b106a3038a99d8
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Mon Mar 15 15:57:46 2010 -0400

    Add completely un-working History driver

commit 1686fbef629baa9025522aed594de043db2e1234
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Mon Mar 15 15:57:28 2010 -0400

    ws

commit 071e8af3fb25d1b0e67b3bc06d49198fbaa7ed80
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Mon Mar 15 15:57:01 2010 -0400

    Fix usage of handle()

commit a142183e8fcbf573fa7e2d2cc0d58b874ce5f2d2
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Mon Mar 15 15:56:22 2010 -0400

    remove obsolete code

commit e40a56caf42e0a96e4d1bb3c17e19dd308279a2b
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Mon Mar 15 15:52:26 2010 -0400

    Add recurring event support

    Still needs work - New recurring events are added currectly in both directions
    but editing exceptions do not yet work correctly.

    Edited on PIM -> Server - The server correctly receives the exception, and stores it, but
     no new event is created for the occurance of the exception.

    Edited on Server -> PIM - A new event is sent to the PIM for the exception, but the
     existing occurance is not removed.

commit 82a5c429bd25107c6388fcb092ef8f85a7f13e82
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Mon Mar 15 15:47:27 2010 -0400

    stub out a test to add for a bug that just wasted an hour of my life

commit 8ea8ae99c30a018a9c57c69f78af8e0324d8742a
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Mon Mar 15 15:46:11 2010 -0400

    Don't forget to update the PingState if this is a PING request.

commit af8a62a35933c9700cd8bd434aa24a537299a2a0
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Mon Mar 15 13:00:53 2010 -0400

    Make Kronolith#listEvents honor the $showRecurrence parameter

commit 4129be1a2517948a3f38437af6416cb57295f29f
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Mon Mar 15 09:58:20 2010 -0400

    Move setNewSyncKey to the base class, other tweaks

commit 1bd2b1b7bcb32b9421954b39485083b38e8d2166
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Mon Mar 15 09:57:24 2010 -0400

    ws

commit 4656c1951d3e7869decdbb67718efde248730f89
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Mon Mar 15 09:34:21 2010 -0400

    ws

commit 609344e8bb2cd546c2f64b045e51673c4fa2a8c0
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Mon Mar 15 09:32:50 2010 -0400

    Notes as I distill more about the AS protocol versions

commit f8a2f1060f4681c73def8848af13b38e9eb6392c
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Sun Mar 14 11:17:28 2010 -0400

    Move PROVISION command handling to Request_Provision

commit 77371eda5feef7cb0416cb36db689cdcd47fb877
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Sun Mar 14 09:44:02 2010 -0400

    Remove code that was moved to individual Request classes

commit 3b4fafb72de8ea1b04ee8b6847351b1bfbfaf8c8
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Sun Mar 14 09:41:04 2010 -0400

    Fix foldersync state handling

    Better performance, one less state file to worry about.

commit ee983e0f4e91fdb80e05a1fdd4205627c6b56242
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Sat Mar 13 16:41:57 2010 -0500

    No need to loadState for FolderSync requests, the getKnownFolders method takes care of it.

commit 37611701f37ed94e9aff3875b4b1aa2ecade78fb
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Sat Mar 13 16:30:34 2010 -0500

    phpdoc

commit 61e0062ab353e76eac595058fcdfb532d73a763e
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Sat Mar 13 16:30:04 2010 -0500

    Fix isConflict() usage

commit cc2f3b5c2ace883b845248d348aaee99e2b21181
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Sat Mar 13 16:26:55 2010 -0500

    phpdoc, style

commit d2e846e48b7cf673ba8e3b0252fa55f14879fd64
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Sat Mar 13 14:34:48 2010 -0500

    Remove the last(?) of the obsolted files

commit cba4d4a98c9b9b104f51b6544456e2b65b806985
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Sat Mar 13 12:04:49 2010 -0500

    Remove deprecated files

commit 86f9cb62aa9e3a2b19347014330cf214eb180a20
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Fri Mar 12 19:37:42 2010 -0500

    Remove unused paramter

commit 3899f8f916df031b2bdd333b9426780b726db8e2
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Fri Mar 12 19:37:18 2010 -0500

    Remove this hack, and implement it correctly.

commit 17b3e4696cbc78668ddf71ec505e6eebabb2e04a
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Fri Mar 12 19:36:01 2010 -0500

    rename loadPingState to loadPingCollectionState to better describe it

    ... also differentiate it from initPingState()

commit 42f29077c2d543ff8789eb853137b1279a43f565
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Fri Mar 12 19:34:49 2010 -0500

    phpdoc

commit 0c1a971c560a4d8d311b54e4949d3d0ca0aa7831
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Fri Mar 12 19:34:16 2010 -0500

    Fix usage of state->updateState()

commit 7991a18fa51122a433aae446008431b3eb1fe553
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Fri Mar 12 19:33:43 2010 -0500

    remove deprecated code

commit 9875dd689ea1c0c97e8754e56530456789f58110
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Fri Mar 12 17:41:45 2010 -0500

    stub out other needed tests

commit b378bc1bee1a6fb176ed3e0f728e9c552d8a350d
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Fri Mar 12 17:37:12 2010 -0500

    Move more constants to class constants

commit 36dbb076054e88e2bca99b89cc06b527db5215a5
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Fri Mar 12 17:36:51 2010 -0500

    Ensure required parameters are available

commit 25cb4f9a32006bd356b9eff5fdc023db42c5119d
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Fri Mar 12 17:36:23 2010 -0500

    ws

commit 822ae27373913aa9a9651d34afb345c672af2b5d
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Fri Mar 12 17:33:55 2010 -0500

    Start adding tests for the file based state machine

commit 2786a9ae1035dd608a433fbf96c1943e45e3ba44
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Thu Mar 11 14:54:52 2010 -0500

    Remove obsolete parameter

commit d3ac1ae688ab5ec2af4cfa334bff543e7eccf2cc
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Thu Mar 11 14:12:32 2010 -0500

    Properly handle OPTIONS request

commit 82b6a6a86743894aced0bff82803fdf92f0b3f9e
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Thu Mar 11 14:11:39 2010 -0500

    Fix state handling for PING requests

commit 66f37afb69450e5e5b08cfa2165228b69bfed078
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Thu Mar 11 14:10:52 2010 -0500

    remove commented code

commit 44e687e0ccc333f6e51401941e9a444f85fb8b67
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Thu Mar 11 12:16:09 2010 -0500

    Start breaking out the logic for handling requests into individual classes.

    The 'major' requests are complete and working. Left the requests that we
    don't currently use in the Active_Sync class so we still have the code,
    but it's not currently used.  Will move those out as they are implemented/tested.

    Likewise, removed deprecated classes from package.xml, but for now, have
    left the files until everything is complete.

commit c95948d87e64b7d12cb2f20f376b4556e99d3374
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Thu Mar 11 00:39:46 2010 -0500

    Check for required parameters, remove old cruft

commit 0b66004e4ba90b24251a7fe7ab86d7decfa32e74
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Thu Mar 11 00:39:19 2010 -0500

    update test

commit d5e9e1b05c8b3a08844988b906dc315f06cf199d
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Thu Mar 11 00:28:45 2010 -0500

    Document the params

commit e42269d2420085c3a8204eca7532f61e5f62e8a4
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Thu Mar 11 00:01:08 2010 -0500

    First round of *massive* refactoring

    This starts moving responsibility for various things around to facilitate
    each storage backend to decide what state storage to use. Moves the majority
    of the functionality that would change to the new State classes. Lots of cruft still
    left here, some of the older state classes will go away, along with some of the importer
    and exporter classes...but this is currently working again for contacts and calendars.

    Committing to get a touchstone for a working state.

commit 51392010d1474909ddf08c24348d51a4a9d26155
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Wed Mar 3 15:52:52 2010 -0500

    Initial commit for new State handler

commit 6a85f9ca827fa77931c950d480f4ca3ba38e02bb
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Mon Feb 22 17:36:50 2010 -0500

    base implementation for getState() and setState()

commit fbe7d2db78e705072d2341799066a091ff30a282
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Mon Feb 22 17:11:42 2010 -0500

    First stage in refactoring state

    Rename getState() to loadState(), and add an (as yet unimplemented)
    method getState() which will return the in-memory state.

    Also rename setState to saveState() which saves the in-memory state
    to storage and re-add a setState() that will
    only set the in-memory state.

commit 1e8046846448e22ae808c73583a5fff4ca6bbd1e
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Sun Feb 21 18:25:53 2010 -0500

    This is actually SYNC_COMMANDS, not SYNC_PERFORM

    Try to keep the code page definitions in line with MS docs

commit 81de70b3be4976ac834e5d1c2e681111078234d0
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Sun Feb 21 10:41:00 2010 -0500

    Finish proper error handling in HandleSync

commit 3ef31a79eb770fa74b469b9d2af4c99ad2f55116
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Sun Feb 21 10:33:20 2010 -0500

    Fix reveresed logic

commit cb537103653fb39f541b45b06cd15649d5278036
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Fri Feb 12 20:20:16 2010 -0500

    This is incorrect. A value of 0x15 on the AirSync codepage is WINDOWSIZE, not MAXITEMS.

    MAXITEMS is only used when dealing with the recipient resolver cache, not with SYNC requests.
    This explains what I *though* was illegial client behaviour, but it was jsut incorrect nomenclature.

commit b76cd952f5dc763a4b78138a6d3ac1faaddfc566
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Fri Feb 12 20:03:47 2010 -0500

    enforce the protocol rule that SUPPORTED tags are only allowed on inital sync (synckey == 0)

commit d39cad04fd744389cc88f1a363029b88f4327571
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Fri Feb 12 20:01:12 2010 -0500

    Start fleshing out support for properly dealing with errors

commit 1cbea9bbea44a695b11ad2b3ff3c9d42c0e9743a
Author: Michael Rubinsky <mrubinsk@yosemite.localdomain>
Date:   Fri Feb 12 19:16:51 2010 -0500

    Fix category support

commit 47a8162cde1dfc824f3d2276c404f557ecddcf50
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Fri Feb 12 18:36:11 2010 -0500

    First attempt at Categories support

commit 2b386986c4c60e5ee78178cac933136141eb35fe
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Fri Feb 12 18:10:08 2010 -0500

    Fill in missing contact attributes .. just 'children' and 'catagories' are missing now

commit 705608f9b7d6a3bde12a875e81d7dfceb008ef6d
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Thu Feb 11 21:02:31 2010 -0500

    Add anniversary field

commit fe3372592d371a639767c6c77c097234d34d9ad0
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Thu Feb 11 21:01:59 2010 -0500

    Better test coverage

commit b25e2356bfeb9fd0fe349baac368e64479f3a5c5
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Thu Feb 11 20:02:38 2010 -0500

    fix property name

commit 6cc2cda15b7fbb6efecee39cc4cd76f055631e23
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Thu Feb 11 18:31:18 2010 -0500

    Enable contact picture syncing.

    Works, but has issues on Android. Android will receive the pitcure information fine,
    but does not SEND the picture data at all. Worse yet, the SYNC_POOMCONTACTS_PICTURE is not
    marked as a ghostable element...but even if it was, Android does not send any SYNC_SUPPORTED
    information during the initial sync. The bottom line is, for now, that picture data will be erased
    on the server anytime a contact with a picture has changes that are transmitted from Android -> Server.

commit 053685a1fb91a654a35ee1a802f8a9e52a088e47
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Thu Feb 11 18:26:10 2010 -0500

    cs, comment cleanup

commit ec775a90122a396164b1f7f2be9eefbf1669690f
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Wed Feb 10 17:30:37 2010 -0500

    Don't get all occurances of recurring events

commit 1b79891794e9acd8e05e692b2f0b4b88a2c98a08
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Wed Feb 10 16:06:29 2010 -0500

    Fix adding events via the API.

    Sneaky little bug. Can't catch a Kronolith_Exception here, since
    Kronolith_Driver#getByUID can also throw a Horde_Exception_NotFound now too.

commit 6b9b4482073700588482fa23d78f0a254dbf459d
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Wed Feb 10 14:37:54 2010 -0500

    Fix variable name

commit 939127c4812cba6d20f9e6fa8202151004de0909
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Wed Feb 10 11:58:50 2010 -0500

    Add timezone support to calendar data.

    Create the activesync TIMEZONE structure based on the user's timezone. Events now properly
    show the correct time on both PIM and Server regardless of what timezone either is set to.
    Recurring event support to follow

commit a3f493455b98c5e0649c24bf0348adea0b756a09
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Wed Feb 10 11:15:47 2010 -0500

    Don't get the policy key if we are not provisioning

commit 97b3e101f87f814529dd830b804732815a07b3b2
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Wed Feb 10 11:14:25 2010 -0500

    ws, cs, etc...

commit 5e7eb678e37381a0fc4d64e749cdc5a79718526e
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Wed Feb 10 01:16:20 2010 -0500

    Timezone utilities for dealing with ActiveSync timezone structures along with unit tests

commit fceac1de1d699d2ff9999d88831b7d46c870afd1
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Tue Feb 9 13:05:51 2010 -0500

    Explain this test, check for the exception and fail() if caught.

commit 4391d58b4caabfae6688e3b974c334c8ec22ffa2
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Tue Feb 9 13:02:47 2010 -0500

    add test to check for proper charset in streamer objects

commit 2bc6fdc478896fce4e65b64427acec5fd1b43070
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Tue Feb 9 12:43:45 2010 -0500

    Fix hash key errors caught by unit tests

commit 400721b219e38ec0b21bcbbb5fc4284ed1f301d5
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Tue Feb 9 12:42:07 2010 -0500

    Update tests to track changes in the Horde driver

commit ff6a7b02c0ff43def766bb2ef2d3849f40568ce9
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Tue Feb 9 12:29:59 2010 -0500

    fix bug caught by unit test

commit c74245817b445c42817aa64f9474a0c5ae77aa31
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Sat Feb 6 17:25:05 2010 -0500

    Don't expect Horde_Rpc to know any details about the ActiveSync protocol

commit ed4088be1a3d1c4044bff4cbb8e7c087515d12dc
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Sat Feb 6 16:07:04 2010 -0500

    Track provisioning changes in Rpc

commit c47af26a42577bfef7e4946304ff1f9123ad8b2e
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Sat Feb 6 16:05:51 2010 -0500

    Add setter for changing the provisioning requirement

commit 76f59e9cc1d1029f071f1c52fac443260e3cbf62
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Sat Feb 6 15:47:08 2010 -0500

    Start implementing PROVISION support.

    Basic PROVISION support is functional, though it doesn't actually check the validity
    of policy keys yet. This will be fixed once state storage is refactored.  Also need to
    add support for configuring the security policies sent. Right now, just a general, very loose
    security policy is sent.

    Tested with the TouchDown client application on Android 2.0.1 since this is the only
    client application I have access to that performs provisioning

commit 51b5c34afe556c788f60f75602c9a135109e0f73
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Sat Feb 6 01:31:11 2010 -0500

    Don't create Horde_Date if we don't have a date

commit 8935865aeb996c0bebba4bd4011ace97b9b88e14
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Sat Feb 6 01:06:25 2010 -0500

    Don't waste cycles creating a vCard just to convert it to something else

    The vCard gives us a nice, known set of attribute names, but is very wastful
    in this case. Just get the attibutes list from contacts/export and convert directly
    to the Streamer object we need...and vice versa

commit 53cfa34a34776a8d1e203615c386d42d61ec81b7
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Sat Feb 6 01:05:30 2010 -0500

    Add support for returning an attributes hash from contacts/export

commit 19dd45e455a07b03e84d8c00ccb79a498170e109
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Sat Feb 6 01:02:39 2010 -0500

    check for failure

commit 0f2e9cbd43df81b59a42f20e3fb35d5875cb1eac
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Fri Feb 5 11:42:47 2010 -0500

    Don't attempt to import a birthday value if there is no value set

commit 346ecf539771e924ab62e0795ae20605130d0f2d
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Fri Feb 5 10:18:32 2010 -0500

    Fix layout of File_Csv pacakge

commit 6410dcfe70207ca8c231d9aef141bffb35323c8c
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Wed Feb 3 18:21:47 2010 -0500

    Allow indicating if we want remote/listTimeObject events from calendar/listEvents

commit 9800e2e8cfa55a77316000ed4366fdca4d1b3a59
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Wed Feb 3 20:20:30 2010 -0500

    Don't request remote or listTimeObject events for activesync requests

commit 040bee3366e14281ee44fb5f3ac5b38c1fbb0842
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Wed Feb 3 13:03:53 2010 -0500

    Don't assume we always have a maxitems tag

commit d69ac2683c56dd5c526d21d6b4faae6b42c15091
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Tue Feb 2 20:39:48 2010 -0500

    ws

commit a3fd012cb0e237bd4e99a5e53c040b97295a8477
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Tue Feb 2 11:27:13 2010 -0500

    Add default value to parameter

    Fixes warnings and notices from the factory

commit e7da02d9c9eac4864b5be1fb3d161814ecb4e552
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Tue Feb 2 11:25:04 2010 -0500

    Fix file name

commit b813844132ce6162137dd81e94ebe2192be23a2e
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Tue Feb 2 10:41:23 2010 -0500

    Comment out the sleep() call again for now, still seems to cause server hangs and other issues with buffered output

commit 58c85f83d1cb40121b121a24471fa13a0212b6a4
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Tue Feb 2 10:40:43 2010 -0500

    Add __get/__set/__isset magic methods to base class

commit 45625ebdb54c29a6c2ca5751ce30b0b2377f337a
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Tue Feb 2 10:39:58 2010 -0500

    Add birthday field support to contacts

commit eb6217f67a8bf418255bff904c16a222949a202f
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Tue Feb 2 10:38:51 2010 -0500

    Add PIM -> Server event creation support

commit 8e80ffebde1bf948c09926c28d31f3162177fcae
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Mon Feb 1 15:11:32 2010 -0500

    Remove apparently unused memebers

commit 5194ef414846879d9711e8663ef20d54b24a5e50
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Mon Feb 1 14:15:07 2010 -0500

    typos

commit 96d610cc8392d4ecceaa3674700f74280503a15c
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Mon Feb 1 14:14:15 2010 -0500

    Add vCalendar parsing for server->pim

commit 83256317edc12b76448d4de476c3f5a5cc3ff9eb
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Mon Feb 1 14:13:57 2010 -0500

    fix api method name

commit 534d832b115360c71191ed6337ab0c52ed94ce62
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Mon Feb 1 14:13:04 2010 -0500

    cs

commit 913c9255d432b6de4669434fa4ffde4cb2475de4
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Mon Feb 1 14:12:05 2010 -0500

    reenable the sleep call, now that the rest of this method is working

commit 39ee598fbf17e6a6faa8507f1c4a338dae0261f8
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Mon Feb 1 11:06:53 2010 -0500

    (re)add missing ChunkContent ajax action

commit 53b53c3692052ba2fc6a9ea28d48ff86de0214cd
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Mon Feb 1 10:16:48 2010 -0500

    clean up log entries, add notes/todos before I forget them

commit a325fc785c66fea0869d66fb19093c5ca7677245
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Mon Feb 1 09:41:19 2010 -0500

    Fix case of method names to horde cs standards

commit 0085e52b83fdd725f02f507cb16aeed5e6a5dc45
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Mon Feb 1 09:39:20 2010 -0500

    Remove this dead code for now, will revist if/when imap support is added

commit cd163a69fc7a97fb9d19d1645e27bdb723e97388
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Mon Feb 1 09:38:54 2010 -0500

    phpdoc

commit a670e3b6cb946f15f5f3e07d02de1eb95da964b9
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Mon Feb 1 09:33:01 2010 -0500

    Create the dependencies in rpc.php

    Still need to clean up the creation, but this moves it out of the Rpc class

commit d2adc24cb999d0a1dacba3c7134b52fe522d190c
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Mon Feb 1 09:31:50 2010 -0500

    Try to figure out how to pass the logger around more cleanly

commit d6963e4ab9d773ba484a60f548cbdc3e01a167f8
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Sun Jan 31 15:36:55 2010 -0500

    fix debug method

commit 7a65db9b4c8a01d658f1f766ef83a69a000816bf
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Sun Jan 31 15:01:05 2010 -0500

    allow multienums to be optionally populated from *_Application::initPrefs() the same as select prefs

commit dc21a9daf7cd2f35171c0a31de8df2bc850124f4
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Sun Jan 31 14:56:43 2010 -0500

    wrapping, update phpdoc

commit d3da7f7c0f9f6962063687d3c004dfa2bd0daba7
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Sun Jan 31 10:29:18 2010 -0500

    Add missing parameter to contacts/replace

    Two way contact syncing is now working :)

commit 7783613e5d82a0c4b1b9b4d1d5cb598ea553a297
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Sun Jan 31 10:28:51 2010 -0500

    typo, remove TODO

commit e0a84ad852f3758bbe6e5d0bd8d61a5253cbffea
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Sun Jan 31 10:27:09 2010 -0500

    Viral typo

commit 77d123025493384c6359ff0ee0fe5b12f2d816a5
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Sun Jan 31 10:26:40 2010 -0500

    ws, cs

commit d86a94c6cd3cd4e17f057ad891f8f0709ab1408c
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Sat Jan 30 17:30:53 2010 -0500

    Better way of detecting what object type we are dealing with...

    One way syncing of contacts from server -> PIM working now

commit 629425f5e0d0fbfbb3400f7ac4a2aec7a7b83bff
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Sat Jan 30 17:13:07 2010 -0500

    Logging, typos, ws, and a quick fix that will be refactored in the next commit

commit 259474126ac77a607bd814e493f7ebfd1c11d99f
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Sat Jan 30 10:48:35 2010 -0500

    Some TODO, small fixes

commit 302036fc40f01b241fc4b654935a95dc53f3b7f3
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Sat Jan 30 10:43:35 2010 -0500

    A bunch of small fixes, cleanups - PIM still not accepting sent messages

commit 9d33cf0e04598396938a50375f0d2f637e8e74ed
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Wed Jan 27 20:31:02 2010 -0500

    These need to call getElement(), not _getElement() so _unGetElement() works as expected

commit 23f541ce964ff4140e3c8cfe1c7696d129542e8e
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Wed Jan 27 20:02:15 2010 -0500

    No longer need these methods

commit abdecae7078caa956980685c599f73d25de2c13d
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Wed Jan 27 19:49:31 2010 -0500

    Parse errors, fix member names, logging etc...and we have a sort-of-working SYNC_FOLDERS command :)

commit c7394d52d020b878497648cb30c16d0a3eda29d4
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Wed Jan 27 12:53:30 2010 -0500

    Add test for GetFolderList command

commit 756fb644d57845e6d24a46463fac2acf233a77fc
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Wed Jan 27 11:33:12 2010 -0500

    Move these header groupings into ActiveSync

    Horde_Rpc shouldn't need to know anything anout the AS protocol

commit 6d402a37aa00477ff05b05ea8282e76fa1964f7b
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Wed Jan 27 11:31:15 2010 -0500

    Need to check for activesync server first, since activesync clients also send and OPTIONS request

commit fd7495f995e09148fe66f754f9e441e41b6d022d
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Mon Jan 25 18:26:38 2010 -0500

    Short circuit method call if no User parameter set

    Also, no reason to keep this response commented out...at least right now.

commit 89e5543ce42475f76db41bc75b98a13cf10fe347
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Mon Jan 25 18:25:19 2010 -0500

    Fix constant names, default value for $_stack

commit 1aa52399d2909d022aaf5cedc7f4b3838c52acde
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Mon Jan 25 18:24:37 2010 -0500

    add missing getState() call, fix phpdoc

commit 2d340e8cece26ef3ef17ba3d9c4ea09c680d6f19
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Mon Jan 25 16:47:43 2010 -0500

    need to inject $registry into the horde connector

commit d81c7bf603eeb44d1c69473a7616daef7d0258e4
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Mon Jan 25 16:47:16 2010 -0500

    Don't attempt to call setLogger() if we don't have one

commit 6c32285ed30fd772b38a17a1a04d0349f9a80956
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Mon Jan 25 16:46:08 2010 -0500

    Fix method names, use a setter for logger

commit f7ec4c79e3b55484f3c81c2ab4edcf664d97a27a
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Mon Jan 25 16:45:14 2010 -0500

    Match the function signature

commit e8bcd27502a3b876e94e085527474e538d7fe8d5
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Mon Jan 25 16:44:49 2010 -0500

    Need to mark this class as abstract

commit 24a554baf19cc9d3c1d881e491b453ca524ff6eb
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Mon Jan 25 16:44:28 2010 -0500

    typo

commit ccab379580af3daa9b2f86dc0d200c8b5b50585b
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Mon Jan 25 16:43:51 2010 -0500

    Fix type hints

commit 4894065458c018a9bb3385b37b8f72cd24be2705
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Mon Jan 25 15:48:22 2010 -0500

    move appInit() to top of file

    Need to instantiate registry  before we instantiate a request object or we end up with warnings about session already started

commit ca2257debd3a2ed7837bf9f858d9803173e0d6e6
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Mon Jan 25 14:57:00 2010 -0500

    fix improper rebase/merge conflict resolution

commit 3d7d018e18fd19f21e1db69bd2d71570b956cbac
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Mon Jan 25 14:41:04 2010 -0500

    I can't seem to type 'connector' without typing 'connectory' for some reason

commit bf9055692aa70240c02c63ca642e99cfffcff444
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Thu Jan 14 12:36:53 2010 -0500

    Inject a Horde_Controller_Request object into Horde_Rpc.

    Use Horde_Controller_Request to obtain all server, post, get, etc..
    values instead of accessing super globals directly. So far, only ActiveSync
    server makes any use of it, the rest of the classes still need to be refactored.

commit dbdad6baae8ec769fee65d843373d964d755b4b1
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Fri Jan 22 18:58:39 2010 -0500

    Remove extra slash now that this url comes from Horde::getServiceLink

    Get rid of these locally defined variables that don't seem to be used

commit dc7e70d592bc2c8d5d1f9ff913dd84148e1953d4
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Wed Jan 20 18:39:14 2010 -0500

    This isn't used anywhere

commit 8755924ab1181e91bb9633bb40b535a91912b2da
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Mon Jan 25 11:46:40 2010 -0500

    Track changes to ActiveSync

    Yes, we will need to inject this stuff, but Horde_Rpc doesn't support it yet.

commit c6bb73124248e746dbbd16c170c72bd4b5fcb720
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Mon Jan 25 11:08:02 2010 -0500

    update package.xml

commit 4eb39d86bdac9149979998c3c8e7e1a14e8f4ab0
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Sun Jan 24 21:36:22 2010 -0500

    Add a changeMessage test

    Sort of works, but can't get the expected text to match the actual
    text of the vCard - think it's in line endings somewhere.

commit 3305f3df71e1c9101f13730a525271b455aec9a7
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Sun Jan 24 21:35:51 2010 -0500

    typos, etc...

commit c421d22e5d5bd0c86dc36c868c868e75bbadf7be
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Sun Jan 24 17:57:56 2010 -0500

    Add testGetMessage()

commit 8dbf25fc37b2bbcd5b950b866cb7f6efab1a425e
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Sun Jan 24 17:57:00 2010 -0500

    Some more small fixes found with unit tests

commit 0d3fe8eb9a79a4e6efc04227d638c8de7630e574
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Sun Jan 24 17:54:52 2010 -0500

    this can be called statically, this avoids warnings in unit tests

commit a7c2a6ec4689e3cc65c71f0888d34bb9144d656d
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Sun Jan 24 17:20:01 2010 -0500

    Initial, very basic unit tests

commit 7496abbf9e109e66434c5c633f6e16481704b537
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Sun Jan 24 17:18:42 2010 -0500

    Fix method visibility in new connector object, misc. other fixes discovered
    from first unit tests

commit 7a938e72435e23324cf5e8d6ddebbb6687aa3827
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Sun Jan 24 16:09:10 2010 -0500

    Abstract out the connector code.

    Separate out the code that calls the registry so it can be injected, easier testing, extending etc...

commit 0f9102b082a46c7d00900930af0d3b8913002592
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Thu Jan 21 17:27:39 2010 -0500

    Remove the rest of the iCalendar parsing code, implement _toVCard() _fromVCard()

    Still need to implement the vCalendar parsing and fill in a lot of misisng fields from
    the vCard, but need to get more familiar with what is actually sent from ActiveSync devices first.

commit 743fa07c0e3b858e4bc7d5bf4ce16a7d0d02d9de
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Wed Jan 20 20:37:00 2010 -0500

    Start replacing vCard/vCalendar functionality with Horde libraries

    also phpdoc, random cleanup

commit 53f63c63b9e5d97713a9c62e3326296c1771021c
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Tue Jan 19 09:57:37 2010 -0500

    package.xml

commit a0b7270a9275f6527a95dfb8bd2cdff54bcad0f2
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Tue Jan 19 09:31:11 2010 -0500

    Shorten these class names also

commit d8a3185eead0498eac888363e1ec211852b418d8
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Tue Jan 19 09:13:45 2010 -0500

    Shorten class names a bit, the 'Sync' didn't add anything to the name's meaning

commit 16f4d870440e9411fb642f5a35df4cc94ad8da7e
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Tue Jan 19 09:09:03 2010 -0500

    Rename base class to *_Base

commit 4a63e9b9af9eb9bd0e3c9b6f3b65bb78a8fc1a0c
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Thu Jan 14 12:36:53 2010 -0500

    Inject a Horde_Controller_Request object into Horde_Rpc.

    Use Horde_Controller_Request to obtain all server, post, get, etc..
    values instead of accessing super globals directly. So far, only ActiveSync
    server makes any use of it, the rest of the classes still need to be refactored.

commit 00ea5c6e9bd905ae5f1f15e6ba7af4b66f5e4fe8
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Sun Jan 17 12:47:48 2010 -0500

    fix sql creation script name

commit fd13ca13997f26e370059f4ce91b1c245ee8b018
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Mon Jan 18 23:23:55 2010 -0500

    Add abstract methods to base class, phpdoc

commit 33008c4d1fb5a926c561864a24f2c428dc7dc15e
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Mon Jan 18 23:09:01 2010 -0500

    Some comments to help me remember what does what..

commit 02f21e5965099348e6298711f4f5eaac742b41e6
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Mon Jan 18 22:40:16 2010 -0500

    ...and another missing method

commit 8f0bcee934dc63388e61b0274583ddba6cca0714
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Mon Jan 18 22:38:06 2010 -0500

    Yet Another method that is missing from Z-Push :/

commit 50401cbf1bcfd428d3a78f5fd12b3d4cda2327d0
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Mon Jan 18 22:06:02 2010 -0500

    Tweak const'r parameters, standardize, use setter for logger

commit 8868c13528086521e173c601c10a595746f426d3
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Mon Jan 18 19:39:12 2010 -0500

    Reorder parameters so $domain is optional

commit 54c61338751147794f3a9f05150353c0c2df4d89
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Mon Jan 18 19:32:20 2010 -0500

    No longer need to pass anything into a const'r for this class

commit c9d69bea7467a395e016b95f9333d1fde162c83e
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Mon Jan 18 19:29:35 2010 -0500

    Don't store the device id in the backend driver, pass it in any methods that really need it

commit 8479524a98c532ee4a8098ca4626f3380341d3e9
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Mon Jan 18 19:14:41 2010 -0500

    Remove protocolversion from the driver classes, doesn't belong there.

    ...and, in fact, was never used there.

commit e88d67839b2dd692995bb5782f7c7dfd4c40bb7f
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Mon Jan 18 19:10:41 2010 -0500

    Call the parent's method also

commit 34864a9afd29eef6f1d521e8c62ae5adebfae119
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Mon Jan 18 19:10:18 2010 -0500

    phpdoc

commit e48b83c4d95a85e2cd810ff1076abf62fc75448d
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Mon Jan 18 19:08:56 2010 -0500

    Rename H_ActiveSync_DiffState to H_ActiveSync_DiffState_Base

    Fill in some phpdoc, clarify some things as I go

commit 87c1d638eb0f9e8fd0691a39932c591fe04a08a0
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Mon Jan 18 19:06:22 2010 -0500

    start filling in debug statements

commit 8e78650100c5b7c25422963e1841a08958ee0a40
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Sun Jan 17 16:29:18 2010 -0500

    Clean up const'r, use setters for the logger, track changes etc..

    Horde_ActiveSync is getting more DI-friendly, but now some of the objects
    are being instantiated in Horde_Rpc - need to fix this as Rpc gets refactored...

commit e4c0b855dfc06b8781e3e6ee5ffb62ca61bcb141
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Sun Jan 17 16:21:15 2010 -0500

    Use the _driver member instead of passing this around everywhere

commit d2d05f7f87e7e00fbbd5df85b12df7fe75d90cfe
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Sun Jan 17 16:20:59 2010 -0500

    use a setter instead of cont'r parameters

commit c08b6a24f571f59c072c06274d8421a28f8163de
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Sun Jan 17 15:46:22 2010 -0500

    Use the StateMachine for saving / getting device ping state

commit c1eb6630f337c52e8ea169471f226b6be59a8bd7
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Sun Jan 17 15:25:43 2010 -0500

    Horde_ActiveSync:: is going to need a H_Controller_Request object also

commit 11a5845843d3c9b273f0ce2e63d02f9f2155911d
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Sun Jan 17 14:59:05 2010 -0500

    ...and actually remove it

commit 4cb9f8a4f8ad4373afa89a1e88ab3b307909e918
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Sun Jan 17 14:57:55 2010 -0500

    Move this method to the only class that uses it

commit c8745a172171538eb19ca1b30f955a805a83d0f0
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Sun Jan 17 14:36:25 2010 -0500

    remove obsolete code, fix global replace error that would have been a real pain to find later...

commit 9363ed3e6629de69017dcb2a7586dd83342b35f7
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Sun Jan 17 14:32:18 2010 -0500

    Remove @todo, the streams are only used in the encoder/decoder so we don't need them here at all

commit 51012e432f3feff33088b53a2b21a70f27b156bf
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Sun Jan 17 14:29:01 2010 -0500

    Inject the encoder and decoder

commit d5f640f72b5d8aa08eb9e307c7bdbc855cb0953b
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Sun Jan 17 11:22:19 2010 -0500

    Remove obsolete methods

commit 72fb56cb9c16b85a6c8b4594f1559f2963138c7c
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Sun Jan 17 11:19:12 2010 -0500

    Add methods to StateMachine to save device folder state

commit 3bd070495bd17746300ba5785feda306d082bfff
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Sun Jan 17 10:24:44 2010 -0500

    remove StateStorage files, track class name changes

commit de70dfae406fc498e5035692b6274a74b5b16d6a
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Sun Jan 17 10:22:09 2010 -0500

    Don't require the syncKey to be known during instantiation.

    Rename StateStorage -> StateMachine
    Don't require the syncKey when object is instantiated, so we can inject the class
    before we know it.

commit ae1ab90fd4366d16bd81532facc7285ddb28378f
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Sat Jan 16 18:57:02 2010 -0500

    Add basic filebased state machine

commit fa012a9062be95a417674c2e79e2d45b0395ad09
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Sat Jan 16 18:22:55 2010 -0500

    State storage that is injectable

    The rest of the code still needs to be refactored to use it...

commit 6be49bbb1aa34ae4de8aa2a5c88582992dc76a74
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Sat Jan 16 18:19:42 2010 -0500

    Clean up the const'r, add initial stub class for a state storage class

commit 3fc220ebd652118d77fef65e5ebb90c2057b72e4
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Sat Jan 16 17:48:17 2010 -0500

    Use type hints in the const'r, and use a setter for the looger

commit 741374a227b2adb5756de3ff9fe156a1b5b0ac5c
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Sat Jan 16 16:34:37 2010 -0500

    Send output back to PDA

commit 2c41718a969a666384e54a1e0926351482e0f7ff
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Sat Jan 16 16:34:05 2010 -0500

    ws, phpdoc

commit 11f132291b5a3e910cd251ea5d9c7232d4c6c979
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Sat Jan 16 16:14:02 2010 -0500

    Have Horde_ActiveSync be responsible for sending ActiveSync specific headers

commit 28209901a1d5fa48bc9577ce34d31e454e9dce20
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Sat Jan 16 14:08:05 2010 -0500

    remove conflict artifact

commit 3d9a54884da6b01942d52ad25686bbe8fcee5ecc
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Thu Jan 14 12:36:53 2010 -0500

    Inject a Horde_Controller_Request object into Horde_Rpc.

    Use Horde_Controller_Request to obtain all server, post, get, etc..
    values instead of accessing super globals directly. So far, only ActiveSync
    server makes any use of it, the rest of the classes still need to be refactored.

commit f33828a01255553bed795bdac1eb1d5f79a888b6
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Thu Jan 14 22:28:34 2010 -0500

    Add basic response handling from Rpc.

    Most of the header ouput currently done here should be moved to the
    Horde_ActiveSync objects once I figure out the best place for them there.

commit 59275a8a2dde5097c25c809ad2c6a6793e7c4970
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Thu Jan 14 22:12:53 2010 -0500

    No need for a separate member for this, just use it more intelligently

commit 07e061dfb654f195053d5f63baefd8865b2462e5
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Thu Jan 14 22:05:11 2010 -0500

    Check for policy header / provisioning requests, phpdoc

commit 538b8f37c5b229fc0b894960953e71c3ff1a93b7
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Thu Jan 14 22:04:31 2010 -0500

    phpdoc, pass in a horde_log_logger

commit 2a7b50fe4bf1b2ab43308b25c176f79d07559219
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Thu Jan 14 14:44:09 2010 -0500

    Authenticate against Horde in the Horde driver.

    Plus some phpdoc, parameter tweaking to better support injection etc... still
    lots to do..

commit 599ec3bb82c464a0b09e79d5a92580e276eeb361
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Thu Jan 14 14:33:25 2010 -0500

    Override Horde_Rpc#authenticate so ActiveSync can handle it

    Allows activesync to authenticate to any backend, not just a horde backend.

commit a88fbd976e9616b0f21f5d0f945a81a4fa821298
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Thu Jan 14 12:56:40 2010 -0500

    Actually, should do this in the const'r

commit 16b9b3f626b5d671275fb63c96a9a97f3cefcde9
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Thu Jan 14 12:54:46 2010 -0500

    Start filling in some logic.

    Check for required GET parameters for ActiveSync Requests

commit fb4aad137d439c609bbc3c5e0fbc7171210f35f7
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Thu Jan 14 12:41:01 2010 -0500

    phpdoc

commit 604c64485e12ad589e2af0bb7c1df27224b2308c
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Thu Jan 14 12:36:53 2010 -0500

    Inject a Horde_Controller_Request object into Horde_Rpc.

    Use Horde_Controller_Request to obtain all server, post, get, etc..
    values instead of accessing super globals directly. So far, only ActiveSync
    server makes any use of it, the rest of the classes still need to be refactored.

commit 63308b3e8366bdfe5bd7ec7d1f494ae57dc43423
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Fri Jan 8 13:28:22 2010 -0500

    Allow RPC backends to handle their own output back to the client.

    Needed since ActiveSync streams results directly back to client while
    the results are being built in order to reduce the memory footprint.

commit 6d0d138e4c84c862b865b205b96bb267b0f7db55
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Tue Jan 5 13:04:46 2010 -0500

    Add stubs for ActiveSync

commit 58ee7245d8b39ef70de481781d53d32c52f11ef1
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Thu Dec 31 13:52:03 2009 -0500

    Add support for detecting ActiveSync requests

commit 175bcc4d6ce2aef28119ed4a939fd8a545048c45
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Tue Jan 5 10:42:21 2010 -0500

    More cleanup for wbxml code, CS, logging, PHP5-ify etc...

commit ab2cfde4b1125a10be0f930e183047f7012e202e
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Mon Jan 4 17:54:12 2010 -0500

    Refactor Z-Push's wbxml handling into Horde_ActiveSync.

    Eventually this should be refactored to use Horde's WBXML library,
    but it's a bit hard to grock for me at the moment.

commit 55e1fa23cec6c03a6552ce2362fb101f902e17f1
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Mon Jan 4 15:42:22 2010 -0500

    More CS, Hordifications, etc...

commit f903e5c39e13d06cd1ec7e421e03c07e7a1831ac
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Mon Jan 4 15:08:09 2010 -0500

    CS, const'r, logging, general Hordify-ing

commit 4df017f7a9b792f6d7f001a96f09b798bcd4bbb5
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Mon Jan 4 11:57:53 2010 -0500

    These don't seem to be used anywhere

commit e712da7baeb4926bcf8c8cab468fb1610a829ce8
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Mon Jan 4 11:55:03 2010 -0500

    Pass logger to DiffState objects and lots of CS cleanup as I see it.

commit f3e6c3ea7dec04a9b11a90c588b8de8a35e98f1e
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Mon Jan 4 10:13:31 2010 -0500

    Pass the logger object to the streamers

commit afd5ceb944f6f23866075c0656832fe86126170e
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Sun Jan 3 16:46:39 2010 -0500

    fix function signature, CS

commit 98139593495a8d66dc62c0c5ba5feacce19ea688
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Sun Jan 3 16:45:43 2010 -0500

    Use a Horde_Log_Logger for debug and error output

commit f2fd2e7704909777b428fc557706fb056a80800e
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Sun Jan 3 13:00:55 2010 -0500

    pass policykey/policies in const'r

commit 6c363b8da893a1e2d9948493849563f965793dc9
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Sun Jan 3 12:52:32 2010 -0500

    Pass username/password in from const'r

commit 955b314f56fea3c67695fdfe806317e6e84a6ad3
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Sun Jan 3 12:33:10 2010 -0500

    pass stateDir and logger in const'r

commit 2df456c43b681c96b62286af89a92ec969dc4199
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Sun Jan 3 12:12:42 2010 -0500

    make input/output streams protected class members

commit e1e3f5631f34989739c409e727f804c4682cea50
Author: Michael J. Rubinsky <mrubinsk@horde.org>
Date:   Sun Jan 3 11:53:08 2010 -0500

    First round of massive refactoring of Z-Push

64 files changed:
ansel/lib/Storage.php
framework/ActiveSync/lib/Horde/ActiveSync.php [new file with mode: 0644]
framework/ActiveSync/lib/Horde/ActiveSync/ActiveSync/Factory.php [new file with mode: 0644]
framework/ActiveSync/lib/Horde/ActiveSync/ContentsCache.php [new file with mode: 0644]
framework/ActiveSync/lib/Horde/ActiveSync/Driver/Base.php [new file with mode: 0644]
framework/ActiveSync/lib/Horde/ActiveSync/Driver/Horde.php [new file with mode: 0644]
framework/ActiveSync/lib/Horde/ActiveSync/Driver/Horde/Connector/Registry.php [new file with mode: 0644]
framework/ActiveSync/lib/Horde/ActiveSync/Exception.php [new file with mode: 0644]
framework/ActiveSync/lib/Horde/ActiveSync/Exporter.php [new file with mode: 0644]
framework/ActiveSync/lib/Horde/ActiveSync/HierarchyCache.php [new file with mode: 0644]
framework/ActiveSync/lib/Horde/ActiveSync/Importer.php [new file with mode: 0644]
framework/ActiveSync/lib/Horde/ActiveSync/Message/Appointment.php [new file with mode: 0644]
framework/ActiveSync/lib/Horde/ActiveSync/Message/Attendee.php [new file with mode: 0644]
framework/ActiveSync/lib/Horde/ActiveSync/Message/Base.php [new file with mode: 0644]
framework/ActiveSync/lib/Horde/ActiveSync/Message/Contact.php [new file with mode: 0644]
framework/ActiveSync/lib/Horde/ActiveSync/Message/Exception.php [new file with mode: 0644]
framework/ActiveSync/lib/Horde/ActiveSync/Message/Folder.php [new file with mode: 0644]
framework/ActiveSync/lib/Horde/ActiveSync/Message/Recurrence.php [new file with mode: 0644]
framework/ActiveSync/lib/Horde/ActiveSync/Request/Base.php [new file with mode: 0644]
framework/ActiveSync/lib/Horde/ActiveSync/Request/FolderSync.php [new file with mode: 0644]
framework/ActiveSync/lib/Horde/ActiveSync/Request/Options.php [new file with mode: 0644]
framework/ActiveSync/lib/Horde/ActiveSync/Request/Ping.php [new file with mode: 0644]
framework/ActiveSync/lib/Horde/ActiveSync/Request/Provision.php [new file with mode: 0644]
framework/ActiveSync/lib/Horde/ActiveSync/Request/Sync.php [new file with mode: 0644]
framework/ActiveSync/lib/Horde/ActiveSync/State/Base.php [new file with mode: 0644]
framework/ActiveSync/lib/Horde/ActiveSync/State/File.php [new file with mode: 0644]
framework/ActiveSync/lib/Horde/ActiveSync/State/History.php [new file with mode: 0644]
framework/ActiveSync/lib/Horde/ActiveSync/Streamer.php [new file with mode: 0644]
framework/ActiveSync/lib/Horde/ActiveSync/Timezone.php [new file with mode: 0644]
framework/ActiveSync/lib/Horde/ActiveSync/Wbxml.php [new file with mode: 0644]
framework/ActiveSync/lib/Horde/ActiveSync/Wbxml/Decoder.php [new file with mode: 0644]
framework/ActiveSync/lib/Horde/ActiveSync/Wbxml/Encoder.php [new file with mode: 0644]
framework/ActiveSync/package.xml [new file with mode: 0644]
framework/ActiveSync/test/Horde/ActiveSync/AllTests.php [new file with mode: 0644]
framework/ActiveSync/test/Horde/ActiveSync/FileStateTest.php [new file with mode: 0644]
framework/ActiveSync/test/Horde/ActiveSync/HordeDriverTest.php [new file with mode: 0644]
framework/ActiveSync/test/Horde/ActiveSync/TimezoneTest.php [new file with mode: 0644]
framework/ActiveSync/test/Horde/ActiveSync/fixtures/MockConnector.php [new file with mode: 0644]
framework/Date/lib/Horde/Date.php
framework/Rpc/lib/Horde/Rpc.php
framework/Rpc/lib/Horde/Rpc/ActiveSync.php [new file with mode: 0644]
framework/Rpc/lib/Horde/Rpc/Jsonrpc.php
framework/Rpc/lib/Horde/Rpc/Phpgw.php
framework/Rpc/lib/Horde/Rpc/Soap.php
framework/Rpc/lib/Horde/Rpc/Webdav.php
framework/Rpc/lib/Horde/Rpc/Xmlrpc.php
framework/Rpc/package.xml
framework/iCalendar/iCalendar/vcard.php
horde/config/conf.xml
horde/rpc.php
kronolith/edit.php
kronolith/lib/Api.php
kronolith/lib/Driver/Sql.php
kronolith/lib/Event.php
kronolith/lib/Event/Sql.php
kronolith/lib/Kronolith.php
kronolith/scripts/sql/kronolith.mssql.sql
kronolith/scripts/sql/kronolith.mysql.sql
kronolith/scripts/sql/kronolith.oci8.sql
kronolith/scripts/sql/kronolith.pgsql.sql
kronolith/scripts/sql/kronolith.sql
kronolith/scripts/sql/kronolith.xml
kronolith/scripts/upgrades/2010-03-25_add_baseid.sql [new file with mode: 0644]
turba/lib/Api.php

index a97dc6a..522a124 100644 (file)
@@ -1163,7 +1163,6 @@ class Ansel_Storage
         $sql = 'SELECT DISTINCT image_location, image_latitude, image_longitude FROM ansel_images WHERE LENGTH(image_location) > 0';
         if (strlen($search)) {
             $sql .= ' AND image_location LIKE ' . $GLOBALS['ansel_db']->quote("$search%");
-
         }
         Horde::logMessage(sprintf("SQL QUERY BY Ansel_Storage::searchLocations: %s", $sql), 'DEBUG');
         $results = $this->_db->query($sql);
diff --git a/framework/ActiveSync/lib/Horde/ActiveSync.php b/framework/ActiveSync/lib/Horde/ActiveSync.php
new file mode 100644 (file)
index 0000000..88f4d54
--- /dev/null
@@ -0,0 +1,1875 @@
+<?php
+/**
+ * ActiveSync Server - ported from ZPush
+ *
+ * Refactoring and other changes are
+ * Copyright 2009 - 2010 The Horde Project (http://www.horde.org)
+ *
+ * @author Michael J. Rubinsky <mrubinsk@horde.org>
+ * @package Horde_ActiveSync
+ */
+/**
+ * File      :   diffbackend.php
+ * Project   :   Z-Push
+ * Descr     :   We do a standard differential
+ *               change detection by sorting both
+ *               lists of items by their unique id,
+ *               and then traversing both arrays
+ *               of items at once. Changes can be
+ *               detected by comparing items at
+ *               the same position in both arrays.
+ *
+ * Created   :   01.10.2007
+ *
+ * Zarafa Deutschland GmbH, www.zarafaserver.de
+ * This file is distributed under GPL v2.
+ * Consult LICENSE file for details
+ */
+// TODO Class constant these:
+define("SYNC_SYNCHRONIZE","Synchronize");
+define("SYNC_REPLIES","Replies");
+define("SYNC_ADD","Add");
+define("SYNC_MODIFY","Modify");
+define("SYNC_REMOVE","Remove");
+define("SYNC_FETCH","Fetch");
+define("SYNC_SYNCKEY","SyncKey");
+define("SYNC_CLIENTENTRYID","ClientEntryId");
+define("SYNC_SERVERENTRYID","ServerEntryId");
+define("SYNC_STATUS","Status");
+define("SYNC_FOLDER","Folder");
+define("SYNC_FOLDERTYPE","FolderType");
+define("SYNC_VERSION","Version");
+define("SYNC_FOLDERID","FolderId");
+define("SYNC_GETCHANGES","GetChanges");
+define("SYNC_MOREAVAILABLE","MoreAvailable");
+define("SYNC_WINDOWSIZE","WindowSize");
+define("SYNC_COMMANDS","Commands");
+define("SYNC_OPTIONS","Options");
+define("SYNC_FILTERTYPE","FilterType");
+define("SYNC_TRUNCATION","Truncation");
+define("SYNC_RTFTRUNCATION","RtfTruncation");
+define("SYNC_CONFLICT","Conflict");
+define("SYNC_FOLDERS","Folders");
+define("SYNC_DATA","Data");
+define("SYNC_DELETESASMOVES","DeletesAsMoves");
+define("SYNC_NOTIFYGUID","NotifyGUID");
+define("SYNC_SUPPORTED","Supported");
+define("SYNC_SOFTDELETE","SoftDelete");
+define("SYNC_MIMESUPPORT","MIMESupport");
+define("SYNC_MIMETRUNCATION","MIMETruncation");
+define("SYNC_NEWMESSAGE","NewMessage");
+
+// POOMCONTACTS
+define("SYNC_POOMCONTACTS_ANNIVERSARY","POOMCONTACTS:Anniversary");
+define("SYNC_POOMCONTACTS_ASSISTANTNAME","POOMCONTACTS:AssistantName");
+define("SYNC_POOMCONTACTS_ASSISTNAMEPHONENUMBER","POOMCONTACTS:AssistnamePhoneNumber");
+define("SYNC_POOMCONTACTS_BIRTHDAY","POOMCONTACTS:Birthday");
+define("SYNC_POOMCONTACTS_BODY","POOMCONTACTS:Body");
+define("SYNC_POOMCONTACTS_BODYSIZE","POOMCONTACTS:BodySize");
+define("SYNC_POOMCONTACTS_BODYTRUNCATED","POOMCONTACTS:BodyTruncated");
+define("SYNC_POOMCONTACTS_BUSINESS2PHONENUMBER","POOMCONTACTS:Business2PhoneNumber");
+define("SYNC_POOMCONTACTS_BUSINESSCITY","POOMCONTACTS:BusinessCity");
+define("SYNC_POOMCONTACTS_BUSINESSCOUNTRY","POOMCONTACTS:BusinessCountry");
+define("SYNC_POOMCONTACTS_BUSINESSPOSTALCODE","POOMCONTACTS:BusinessPostalCode");
+define("SYNC_POOMCONTACTS_BUSINESSSTATE","POOMCONTACTS:BusinessState");
+define("SYNC_POOMCONTACTS_BUSINESSSTREET","POOMCONTACTS:BusinessStreet");
+define("SYNC_POOMCONTACTS_BUSINESSFAXNUMBER","POOMCONTACTS:BusinessFaxNumber");
+define("SYNC_POOMCONTACTS_BUSINESSPHONENUMBER","POOMCONTACTS:BusinessPhoneNumber");
+define("SYNC_POOMCONTACTS_CARPHONENUMBER","POOMCONTACTS:CarPhoneNumber");
+define("SYNC_POOMCONTACTS_CATEGORIES","POOMCONTACTS:Categories");
+define("SYNC_POOMCONTACTS_CATEGORY","POOMCONTACTS:Category");
+define("SYNC_POOMCONTACTS_CHILDREN","POOMCONTACTS:Children");
+define("SYNC_POOMCONTACTS_CHILD","POOMCONTACTS:Child");
+define("SYNC_POOMCONTACTS_COMPANYNAME","POOMCONTACTS:CompanyName");
+define("SYNC_POOMCONTACTS_DEPARTMENT","POOMCONTACTS:Department");
+define("SYNC_POOMCONTACTS_EMAIL1ADDRESS","POOMCONTACTS:Email1Address");
+define("SYNC_POOMCONTACTS_EMAIL2ADDRESS","POOMCONTACTS:Email2Address");
+define("SYNC_POOMCONTACTS_EMAIL3ADDRESS","POOMCONTACTS:Email3Address");
+define("SYNC_POOMCONTACTS_FILEAS","POOMCONTACTS:FileAs");
+define("SYNC_POOMCONTACTS_FIRSTNAME","POOMCONTACTS:FirstName");
+define("SYNC_POOMCONTACTS_HOME2PHONENUMBER","POOMCONTACTS:Home2PhoneNumber");
+define("SYNC_POOMCONTACTS_HOMECITY","POOMCONTACTS:HomeCity");
+define("SYNC_POOMCONTACTS_HOMECOUNTRY","POOMCONTACTS:HomeCountry");
+define("SYNC_POOMCONTACTS_HOMEPOSTALCODE","POOMCONTACTS:HomePostalCode");
+define("SYNC_POOMCONTACTS_HOMESTATE","POOMCONTACTS:HomeState");
+define("SYNC_POOMCONTACTS_HOMESTREET","POOMCONTACTS:HomeStreet");
+define("SYNC_POOMCONTACTS_HOMEFAXNUMBER","POOMCONTACTS:HomeFaxNumber");
+define("SYNC_POOMCONTACTS_HOMEPHONENUMBER","POOMCONTACTS:HomePhoneNumber");
+define("SYNC_POOMCONTACTS_JOBTITLE","POOMCONTACTS:JobTitle");
+define("SYNC_POOMCONTACTS_LASTNAME","POOMCONTACTS:LastName");
+define("SYNC_POOMCONTACTS_MIDDLENAME","POOMCONTACTS:MiddleName");
+define("SYNC_POOMCONTACTS_MOBILEPHONENUMBER","POOMCONTACTS:MobilePhoneNumber");
+define("SYNC_POOMCONTACTS_OFFICELOCATION","POOMCONTACTS:OfficeLocation");
+define("SYNC_POOMCONTACTS_OTHERCITY","POOMCONTACTS:OtherCity");
+define("SYNC_POOMCONTACTS_OTHERCOUNTRY","POOMCONTACTS:OtherCountry");
+define("SYNC_POOMCONTACTS_OTHERPOSTALCODE","POOMCONTACTS:OtherPostalCode");
+define("SYNC_POOMCONTACTS_OTHERSTATE","POOMCONTACTS:OtherState");
+define("SYNC_POOMCONTACTS_OTHERSTREET","POOMCONTACTS:OtherStreet");
+define("SYNC_POOMCONTACTS_PAGERNUMBER","POOMCONTACTS:PagerNumber");
+define("SYNC_POOMCONTACTS_RADIOPHONENUMBER","POOMCONTACTS:RadioPhoneNumber");
+define("SYNC_POOMCONTACTS_SPOUSE","POOMCONTACTS:Spouse");
+define("SYNC_POOMCONTACTS_SUFFIX","POOMCONTACTS:Suffix");
+define("SYNC_POOMCONTACTS_TITLE","POOMCONTACTS:Title");
+define("SYNC_POOMCONTACTS_WEBPAGE","POOMCONTACTS:WebPage");
+define("SYNC_POOMCONTACTS_YOMICOMPANYNAME","POOMCONTACTS:YomiCompanyName");
+define("SYNC_POOMCONTACTS_YOMIFIRSTNAME","POOMCONTACTS:YomiFirstName");
+define("SYNC_POOMCONTACTS_YOMILASTNAME","POOMCONTACTS:YomiLastName");
+define("SYNC_POOMCONTACTS_RTF","POOMCONTACTS:Rtf");
+define("SYNC_POOMCONTACTS_PICTURE","POOMCONTACTS:Picture");
+
+// POOMMAIL
+define("SYNC_POOMMAIL_ATTACHMENT","POOMMAIL:Attachment");
+define("SYNC_POOMMAIL_ATTACHMENTS","POOMMAIL:Attachments");
+define("SYNC_POOMMAIL_ATTNAME","POOMMAIL:AttName");
+define("SYNC_POOMMAIL_ATTSIZE","POOMMAIL:AttSize");
+define("SYNC_POOMMAIL_ATTOID","POOMMAIL:AttOid");
+define("SYNC_POOMMAIL_ATTMETHOD","POOMMAIL:AttMethod");
+define("SYNC_POOMMAIL_ATTREMOVED","POOMMAIL:AttRemoved");
+define("SYNC_POOMMAIL_BODY","POOMMAIL:Body");
+define("SYNC_POOMMAIL_BODYSIZE","POOMMAIL:BodySize");
+define("SYNC_POOMMAIL_BODYTRUNCATED","POOMMAIL:BodyTruncated");
+define("SYNC_POOMMAIL_DATERECEIVED","POOMMAIL:DateReceived");
+define("SYNC_POOMMAIL_DISPLAYNAME","POOMMAIL:DisplayName");
+define("SYNC_POOMMAIL_DISPLAYTO","POOMMAIL:DisplayTo");
+define("SYNC_POOMMAIL_IMPORTANCE","POOMMAIL:Importance");
+define("SYNC_POOMMAIL_MESSAGECLASS","POOMMAIL:MessageClass");
+define("SYNC_POOMMAIL_SUBJECT","POOMMAIL:Subject");
+define("SYNC_POOMMAIL_READ","POOMMAIL:Read");
+define("SYNC_POOMMAIL_TO","POOMMAIL:To");
+define("SYNC_POOMMAIL_CC","POOMMAIL:Cc");
+define("SYNC_POOMMAIL_FROM","POOMMAIL:From");
+define("SYNC_POOMMAIL_REPLY_TO","POOMMAIL:Reply-To");
+define("SYNC_POOMMAIL_ALLDAYEVENT","POOMMAIL:AllDayEvent");
+define("SYNC_POOMMAIL_CATEGORIES","POOMMAIL:Categories");
+define("SYNC_POOMMAIL_CATEGORY","POOMMAIL:Category");
+define("SYNC_POOMMAIL_DTSTAMP","POOMMAIL:DtStamp");
+define("SYNC_POOMMAIL_ENDTIME","POOMMAIL:EndTime");
+define("SYNC_POOMMAIL_INSTANCETYPE","POOMMAIL:InstanceType");
+define("SYNC_POOMMAIL_BUSYSTATUS","POOMMAIL:BusyStatus");
+define("SYNC_POOMMAIL_LOCATION","POOMMAIL:Location");
+define("SYNC_POOMMAIL_MEETINGREQUEST","POOMMAIL:MeetingRequest");
+define("SYNC_POOMMAIL_ORGANIZER","POOMMAIL:Organizer");
+define("SYNC_POOMMAIL_RECURRENCEID","POOMMAIL:RecurrenceId");
+define("SYNC_POOMMAIL_REMINDER","POOMMAIL:Reminder");
+define("SYNC_POOMMAIL_RESPONSEREQUESTED","POOMMAIL:ResponseRequested");
+define("SYNC_POOMMAIL_RECURRENCES","POOMMAIL:Recurrences");
+define("SYNC_POOMMAIL_RECURRENCE","POOMMAIL:Recurrence");
+define("SYNC_POOMMAIL_TYPE","POOMMAIL:Type");
+define("SYNC_POOMMAIL_UNTIL","POOMMAIL:Until");
+define("SYNC_POOMMAIL_OCCURRENCES","POOMMAIL:Occurrences");
+define("SYNC_POOMMAIL_INTERVAL","POOMMAIL:Interval");
+define("SYNC_POOMMAIL_DAYOFWEEK","POOMMAIL:DayOfWeek");
+define("SYNC_POOMMAIL_DAYOFMONTH","POOMMAIL:DayOfMonth");
+define("SYNC_POOMMAIL_WEEKOFMONTH","POOMMAIL:WeekOfMonth");
+define("SYNC_POOMMAIL_MONTHOFYEAR","POOMMAIL:MonthOfYear");
+define("SYNC_POOMMAIL_STARTTIME","POOMMAIL:StartTime");
+define("SYNC_POOMMAIL_SENSITIVITY","POOMMAIL:Sensitivity");
+define("SYNC_POOMMAIL_TIMEZONE","POOMMAIL:TimeZone");
+define("SYNC_POOMMAIL_GLOBALOBJID","POOMMAIL:GlobalObjId");
+define("SYNC_POOMMAIL_THREADTOPIC","POOMMAIL:ThreadTopic");
+define("SYNC_POOMMAIL_MIMEDATA","POOMMAIL:MIMEData");
+define("SYNC_POOMMAIL_MIMETRUNCATED","POOMMAIL:MIMETruncated");
+define("SYNC_POOMMAIL_MIMESIZE","POOMMAIL:MIMESize");
+define("SYNC_POOMMAIL_INTERNETCPID","POOMMAIL:InternetCPID");
+
+// AIRNOTIFY
+define("SYNC_AIRNOTIFY_NOTIFY","AirNotify:Notify");
+define("SYNC_AIRNOTIFY_NOTIFICATION","AirNotify:Notification");
+define("SYNC_AIRNOTIFY_VERSION","AirNotify:Version");
+define("SYNC_AIRNOTIFY_LIFETIME","AirNotify:Lifetime");
+define("SYNC_AIRNOTIFY_DEVICEINFO","AirNotify:DeviceInfo");
+define("SYNC_AIRNOTIFY_ENABLE","AirNotify:Enable");
+define("SYNC_AIRNOTIFY_FOLDER","AirNotify:Folder");
+define("SYNC_AIRNOTIFY_SERVERENTRYID","AirNotify:ServerEntryId");
+define("SYNC_AIRNOTIFY_DEVICEADDRESS","AirNotify:DeviceAddress");
+define("SYNC_AIRNOTIFY_VALIDCARRIERPROFILES","AirNotify:ValidCarrierProfiles");
+define("SYNC_AIRNOTIFY_CARRIERPROFILE","AirNotify:CarrierProfile");
+define("SYNC_AIRNOTIFY_STATUS","AirNotify:Status");
+define("SYNC_AIRNOTIFY_REPLIES","AirNotify:Replies");
+define("SYNC_AIRNOTIFY_VERSION='1.1'","AirNotify:Version='1.1'");
+define("SYNC_AIRNOTIFY_DEVICES","AirNotify:Devices");
+define("SYNC_AIRNOTIFY_DEVICE","AirNotify:Device");
+define("SYNC_AIRNOTIFY_ID","AirNotify:Id");
+define("SYNC_AIRNOTIFY_EXPIRY","AirNotify:Expiry");
+define("SYNC_AIRNOTIFY_NOTIFYGUID","AirNotify:NotifyGUID");
+
+// POOMCAL
+define("SYNC_POOMCAL_TIMEZONE","POOMCAL:Timezone");
+define("SYNC_POOMCAL_ALLDAYEVENT","POOMCAL:AllDayEvent");
+define("SYNC_POOMCAL_ATTENDEES","POOMCAL:Attendees");
+define("SYNC_POOMCAL_ATTENDEE","POOMCAL:Attendee");
+define("SYNC_POOMCAL_EMAIL","POOMCAL:Email");
+define("SYNC_POOMCAL_NAME","POOMCAL:Name");
+define("SYNC_POOMCAL_BODY","POOMCAL:Body");
+define("SYNC_POOMCAL_BODYTRUNCATED","POOMCAL:BodyTruncated");
+define("SYNC_POOMCAL_BUSYSTATUS","POOMCAL:BusyStatus");
+define("SYNC_POOMCAL_CATEGORIES","POOMCAL:Categories");
+define("SYNC_POOMCAL_CATEGORY","POOMCAL:Category");
+define("SYNC_POOMCAL_RTF","POOMCAL:Rtf");
+define("SYNC_POOMCAL_DTSTAMP","POOMCAL:DtStamp");
+define("SYNC_POOMCAL_ENDTIME","POOMCAL:EndTime");
+define("SYNC_POOMCAL_EXCEPTION","POOMCAL:Exception");
+define("SYNC_POOMCAL_EXCEPTIONS","POOMCAL:Exceptions");
+define("SYNC_POOMCAL_DELETED","POOMCAL:Deleted");
+define("SYNC_POOMCAL_EXCEPTIONSTARTTIME","POOMCAL:ExceptionStartTime");
+define("SYNC_POOMCAL_LOCATION","POOMCAL:Location");
+define("SYNC_POOMCAL_MEETINGSTATUS","POOMCAL:MeetingStatus");
+define("SYNC_POOMCAL_ORGANIZEREMAIL","POOMCAL:OrganizerEmail");
+define("SYNC_POOMCAL_ORGANIZERNAME","POOMCAL:OrganizerName");
+define("SYNC_POOMCAL_RECURRENCE","POOMCAL:Recurrence");
+define("SYNC_POOMCAL_TYPE","POOMCAL:Type");
+define("SYNC_POOMCAL_UNTIL","POOMCAL:Until");
+define("SYNC_POOMCAL_OCCURRENCES","POOMCAL:Occurrences");
+define("SYNC_POOMCAL_INTERVAL","POOMCAL:Interval");
+define("SYNC_POOMCAL_DAYOFWEEK","POOMCAL:DayOfWeek");
+define("SYNC_POOMCAL_DAYOFMONTH","POOMCAL:DayOfMonth");
+define("SYNC_POOMCAL_WEEKOFMONTH","POOMCAL:WeekOfMonth");
+define("SYNC_POOMCAL_MONTHOFYEAR","POOMCAL:MonthOfYear");
+define("SYNC_POOMCAL_REMINDER","POOMCAL:Reminder");
+define("SYNC_POOMCAL_SENSITIVITY","POOMCAL:Sensitivity");
+define("SYNC_POOMCAL_SUBJECT","POOMCAL:Subject");
+define("SYNC_POOMCAL_STARTTIME","POOMCAL:StartTime");
+define("SYNC_POOMCAL_UID","POOMCAL:UID");
+define("SYNC_POOMCAL_RESPONSETYPE", "POOMCAL:ResponseType");
+
+// Move
+define("SYNC_MOVE_MOVES","Move:Moves");
+define("SYNC_MOVE_MOVE","Move:Move");
+define("SYNC_MOVE_SRCMSGID","Move:SrcMsgId");
+define("SYNC_MOVE_SRCFLDID","Move:SrcFldId");
+define("SYNC_MOVE_DSTFLDID","Move:DstFldId");
+define("SYNC_MOVE_RESPONSE","Move:Response");
+define("SYNC_MOVE_STATUS","Move:Status");
+define("SYNC_MOVE_DSTMSGID","Move:DstMsgId");
+
+// GetItemEstimate
+define("SYNC_GETITEMESTIMATE_GETITEMESTIMATE","GetItemEstimate:GetItemEstimate");
+define("SYNC_GETITEMESTIMATE_VERSION","GetItemEstimate:Version");
+define("SYNC_GETITEMESTIMATE_FOLDERS","GetItemEstimate:Folders");
+define("SYNC_GETITEMESTIMATE_FOLDER","GetItemEstimate:Folder");
+define("SYNC_GETITEMESTIMATE_FOLDERTYPE","GetItemEstimate:FolderType");
+define("SYNC_GETITEMESTIMATE_FOLDERID","GetItemEstimate:FolderId");
+define("SYNC_GETITEMESTIMATE_DATETIME","GetItemEstimate:DateTime");
+define("SYNC_GETITEMESTIMATE_ESTIMATE","GetItemEstimate:Estimate");
+define("SYNC_GETITEMESTIMATE_RESPONSE","GetItemEstimate:Response");
+define("SYNC_GETITEMESTIMATE_STATUS","GetItemEstimate:Status");
+
+// FolderHierarchy
+define("SYNC_FOLDERHIERARCHY_FOLDERS","FolderHierarchy:Folders");
+define("SYNC_FOLDERHIERARCHY_FOLDER","FolderHierarchy:Folder");
+define("SYNC_FOLDERHIERARCHY_DISPLAYNAME","FolderHierarchy:DisplayName");
+define("SYNC_FOLDERHIERARCHY_SERVERENTRYID","FolderHierarchy:ServerEntryId");
+define("SYNC_FOLDERHIERARCHY_PARENTID","FolderHierarchy:ParentId");
+define("SYNC_FOLDERHIERARCHY_TYPE","FolderHierarchy:Type");
+define("SYNC_FOLDERHIERARCHY_RESPONSE","FolderHierarchy:Response");
+define("SYNC_FOLDERHIERARCHY_STATUS","FolderHierarchy:Status");
+define("SYNC_FOLDERHIERARCHY_CONTENTCLASS","FolderHierarchy:ContentClass");
+define("SYNC_FOLDERHIERARCHY_CHANGES","FolderHierarchy:Changes");
+define("SYNC_FOLDERHIERARCHY_ADD","FolderHierarchy:Add");
+define("SYNC_FOLDERHIERARCHY_REMOVE","FolderHierarchy:Remove");
+define("SYNC_FOLDERHIERARCHY_UPDATE","FolderHierarchy:Update");
+define("SYNC_FOLDERHIERARCHY_SYNCKEY","FolderHierarchy:SyncKey");
+define("SYNC_FOLDERHIERARCHY_FOLDERCREATE","FolderHierarchy:FolderCreate");
+define("SYNC_FOLDERHIERARCHY_FOLDERDELETE","FolderHierarchy:FolderDelete");
+define("SYNC_FOLDERHIERARCHY_FOLDERUPDATE","FolderHierarchy:FolderUpdate");
+define("SYNC_FOLDERHIERARCHY_FOLDERSYNC","FolderHierarchy:FolderSync");
+define("SYNC_FOLDERHIERARCHY_COUNT","FolderHierarchy:Count");
+define("SYNC_FOLDERHIERARCHY_VERSION","FolderHierarchy:Version");
+
+// MeetingResponse
+define("SYNC_MEETINGRESPONSE_CALENDARID","MeetingResponse:CalendarId");
+define("SYNC_MEETINGRESPONSE_FOLDERID","MeetingResponse:FolderId");
+define("SYNC_MEETINGRESPONSE_MEETINGRESPONSE","MeetingResponse:MeetingResponse");
+define("SYNC_MEETINGRESPONSE_REQUESTID","MeetingResponse:RequestId");
+define("SYNC_MEETINGRESPONSE_REQUEST","MeetingResponse:Request");
+define("SYNC_MEETINGRESPONSE_RESULT","MeetingResponse:Result");
+define("SYNC_MEETINGRESPONSE_STATUS","MeetingResponse:Status");
+define("SYNC_MEETINGRESPONSE_USERRESPONSE","MeetingResponse:UserResponse");
+define("SYNC_MEETINGRESPONSE_VERSION","MeetingResponse:Version");
+
+// POOMTASKS
+define("SYNC_POOMTASKS_BODY","POOMTASKS:Body");
+define("SYNC_POOMTASKS_BODYSIZE","POOMTASKS:BodySize");
+define("SYNC_POOMTASKS_BODYTRUNCATED","POOMTASKS:BodyTruncated");
+define("SYNC_POOMTASKS_CATEGORIES","POOMTASKS:Categories");
+define("SYNC_POOMTASKS_CATEGORY","POOMTASKS:Category");
+define("SYNC_POOMTASKS_COMPLETE","POOMTASKS:Complete");
+define("SYNC_POOMTASKS_DATECOMPLETED","POOMTASKS:DateCompleted");
+define("SYNC_POOMTASKS_DUEDATE","POOMTASKS:DueDate");
+define("SYNC_POOMTASKS_UTCDUEDATE","POOMTASKS:UtcDueDate");
+define("SYNC_POOMTASKS_IMPORTANCE","POOMTASKS:Importance");
+define("SYNC_POOMTASKS_RECURRENCE","POOMTASKS:Recurrence");
+define("SYNC_POOMTASKS_TYPE","POOMTASKS:Type");
+define("SYNC_POOMTASKS_START","POOMTASKS:Start");
+define("SYNC_POOMTASKS_UNTIL","POOMTASKS:Until");
+define("SYNC_POOMTASKS_OCCURRENCES","POOMTASKS:Occurrences");
+define("SYNC_POOMTASKS_INTERVAL","POOMTASKS:Interval");
+define("SYNC_POOMTASKS_DAYOFWEEK","POOMTASKS:DayOfWeek");
+define("SYNC_POOMTASKS_DAYOFMONTH","POOMTASKS:DayOfMonth");
+define("SYNC_POOMTASKS_WEEKOFMONTH","POOMTASKS:WeekOfMonth");
+define("SYNC_POOMTASKS_MONTHOFYEAR","POOMTASKS:MonthOfYear");
+define("SYNC_POOMTASKS_REGENERATE","POOMTASKS:Regenerate");
+define("SYNC_POOMTASKS_DEADOCCUR","POOMTASKS:DeadOccur");
+define("SYNC_POOMTASKS_REMINDERSET","POOMTASKS:ReminderSet");
+define("SYNC_POOMTASKS_REMINDERTIME","POOMTASKS:ReminderTime");
+define("SYNC_POOMTASKS_SENSITIVITY","POOMTASKS:Sensitivity");
+define("SYNC_POOMTASKS_STARTDATE","POOMTASKS:StartDate");
+define("SYNC_POOMTASKS_UTCSTARTDATE","POOMTASKS:UtcStartDate");
+define("SYNC_POOMTASKS_SUBJECT","POOMTASKS:Subject");
+define("SYNC_POOMTASKS_RTF","POOMTASKS:Rtf");
+
+// ResolveRecipients
+define("SYNC_RESOLVERECIPIENTS_RESOLVERECIPIENTS","ResolveRecipients:ResolveRecipients");
+define("SYNC_RESOLVERECIPIENTS_RESPONSE","ResolveRecipients:Response");
+define("SYNC_RESOLVERECIPIENTS_STATUS","ResolveRecipients:Status");
+define("SYNC_RESOLVERECIPIENTS_TYPE","ResolveRecipients:Type");
+define("SYNC_RESOLVERECIPIENTS_RECIPIENT","ResolveRecipients:Recipient");
+define("SYNC_RESOLVERECIPIENTS_DISPLAYNAME","ResolveRecipients:DisplayName");
+define("SYNC_RESOLVERECIPIENTS_EMAILADDRESS","ResolveRecipients:EmailAddress");
+define("SYNC_RESOLVERECIPIENTS_CERTIFICATES","ResolveRecipients:Certificates");
+define("SYNC_RESOLVERECIPIENTS_CERTIFICATE","ResolveRecipients:Certificate");
+define("SYNC_RESOLVERECIPIENTS_MINICERTIFICATE","ResolveRecipients:MiniCertificate");
+define("SYNC_RESOLVERECIPIENTS_OPTIONS","ResolveRecipients:Options");
+define("SYNC_RESOLVERECIPIENTS_TO","ResolveRecipients:To");
+define("SYNC_RESOLVERECIPIENTS_CERTIFICATERETRIEVAL","ResolveRecipients:CertificateRetrieval");
+define("SYNC_RESOLVERECIPIENTS_RECIPIENTCOUNT","ResolveRecipients:RecipientCount");
+define("SYNC_RESOLVERECIPIENTS_MAXCERTIFICATES","ResolveRecipients:MaxCertificates");
+define("SYNC_RESOLVERECIPIENTS_MAXAMBIGUOUSRECIPIENTS","ResolveRecipients:MaxAmbiguousRecipients");
+define("SYNC_RESOLVERECIPIENTS_CERTIFICATECOUNT","ResolveRecipients:CertificateCount");
+
+// ValidateCert
+define("SYNC_VALIDATECERT_VALIDATECERT","ValidateCert:ValidateCert");
+define("SYNC_VALIDATECERT_CERTIFICATES","ValidateCert:Certificates");
+define("SYNC_VALIDATECERT_CERTIFICATE","ValidateCert:Certificate");
+define("SYNC_VALIDATECERT_CERTIFICATECHAIN","ValidateCert:CertificateChain");
+define("SYNC_VALIDATECERT_CHECKCRL","ValidateCert:CheckCRL");
+define("SYNC_VALIDATECERT_STATUS","ValidateCert:Status");
+
+// POOMCONTACTS2
+define("SYNC_POOMCONTACTS2_CUSTOMERID","POOMCONTACTS2:CustomerId");
+define("SYNC_POOMCONTACTS2_GOVERNMENTID","POOMCONTACTS2:GovernmentId");
+define("SYNC_POOMCONTACTS2_IMADDRESS","POOMCONTACTS2:IMAddress");
+define("SYNC_POOMCONTACTS2_IMADDRESS2","POOMCONTACTS2:IMAddress2");
+define("SYNC_POOMCONTACTS2_IMADDRESS3","POOMCONTACTS2:IMAddress3");
+define("SYNC_POOMCONTACTS2_MANAGERNAME","POOMCONTACTS2:ManagerName");
+define("SYNC_POOMCONTACTS2_COMPANYMAINPHONE","POOMCONTACTS2:CompanyMainPhone");
+define("SYNC_POOMCONTACTS2_ACCOUNTNAME","POOMCONTACTS2:AccountName");
+define("SYNC_POOMCONTACTS2_NICKNAME","POOMCONTACTS2:NickName");
+define("SYNC_POOMCONTACTS2_MMS","POOMCONTACTS2:MMS");
+
+// Ping
+define("SYNC_PING_PING","Ping:Ping");
+define("SYNC_PING_STATUS","Ping:Status");
+define("SYNC_PING_LIFETIME", "Ping:LifeTime");
+define("SYNC_PING_FOLDERS", "Ping:Folders");
+define("SYNC_PING_FOLDER", "Ping:Folder");
+define("SYNC_PING_SERVERENTRYID", "Ping:ServerEntryId");
+define("SYNC_PING_FOLDERTYPE", "Ping:FolderType");
+
+//Provision
+define("SYNC_PROVISION_PROVISION", "Provision:Provision");
+define("SYNC_PROVISION_POLICIES", "Provision:Policies");
+define("SYNC_PROVISION_POLICY", "Provision:Policy");
+define("SYNC_PROVISION_POLICYTYPE", "Provision:PolicyType");
+define("SYNC_PROVISION_POLICYKEY", "Provision:PolicyKey");
+define("SYNC_PROVISION_DATA", "Provision:Data");
+define("SYNC_PROVISION_STATUS", "Provision:Status");
+define("SYNC_PROVISION_REMOTEWIPE", "Provision:RemoteWipe");
+define("SYNC_PROVISION_EASPROVISIONDOC", "Provision:EASProvisionDoc");
+
+//Search
+define("SYNC_SEARCH_SEARCH", "Search:Search");
+define("SYNC_SEARCH_STORE", "Search:Store");
+define("SYNC_SEARCH_NAME", "Search:Name");
+define("SYNC_SEARCH_QUERY", "Search:Query");
+define("SYNC_SEARCH_OPTIONS", "Search:Options");
+define("SYNC_SEARCH_RANGE", "Search:Range");
+define("SYNC_SEARCH_STATUS", "Search:Status");
+define("SYNC_SEARCH_RESPONSE", "Search:Response");
+define("SYNC_SEARCH_RESULT", "Search:Result");
+define("SYNC_SEARCH_PROPERTIES", "Search:Properties");
+define("SYNC_SEARCH_TOTAL", "Search:Total");
+define("SYNC_SEARCH_EQUALTO", "Search:EqualTo");
+define("SYNC_SEARCH_VALUE", "Search:Value");
+define("SYNC_SEARCH_AND", "Search:And");
+define("SYNC_SEARCH_OR", "Search:Or");
+define("SYNC_SEARCH_FREETEXT", "Search:FreeText");
+define("SYNC_SEARCH_DEEPTRAVERSAL", "Search:DeepTraversal");
+define("SYNC_SEARCH_LONGID", "Search:LongId");
+define("SYNC_SEARCH_REBUILDRESULTS", "Search:RebuildResults");
+define("SYNC_SEARCH_LESSTHAN", "Search:LessThan");
+define("SYNC_SEARCH_GREATERTHAN", "Search:GreaterThan");
+define("SYNC_SEARCH_SCHEMA", "Search:Schema");
+define("SYNC_SEARCH_SUPPORTED", "Search:Supported");
+
+//GAL
+define("SYNC_GAL_DISPLAYNAME", "GAL:DisplayName");
+define("SYNC_GAL_PHONE", "GAL:Phone");
+define("SYNC_GAL_OFFICE", "GAL:Office");
+define("SYNC_GAL_TITLE", "GAL:Title");
+define("SYNC_GAL_COMPANY", "GAL:Company");
+define("SYNC_GAL_ALIAS", "GAL:Alias");
+define("SYNC_GAL_FIRSTNAME", "GAL:FirstName");
+define("SYNC_GAL_LASTNAME", "GAL:LastName");
+define("SYNC_GAL_HOMEPHONE", "GAL:HomePhone");
+define("SYNC_GAL_MOBILEPHONE", "GAL:MobilePhone");
+define("SYNC_GAL_EMAILADDRESS", "GAL:EmailAddress");
+
+// Other constants
+define("SYNC_FOLDER_TYPE_OTHER", 1);
+define("SYNC_FOLDER_TYPE_INBOX", 2);
+define("SYNC_FOLDER_TYPE_DRAFTS", 3);
+define("SYNC_FOLDER_TYPE_WASTEBASKET", 4);
+define("SYNC_FOLDER_TYPE_SENTMAIL", 5);
+define("SYNC_FOLDER_TYPE_OUTBOX", 6);
+define("SYNC_FOLDER_TYPE_TASK", 7);
+define("SYNC_FOLDER_TYPE_APPOINTMENT", 8);
+define("SYNC_FOLDER_TYPE_CONTACT", 9);
+define("SYNC_FOLDER_TYPE_NOTE", 10);
+define("SYNC_FOLDER_TYPE_JOURNAL", 11);
+define("SYNC_FOLDER_TYPE_USER_MAIL", 12);
+define("SYNC_FOLDER_TYPE_USER_APPOINTMENT", 13);
+define("SYNC_FOLDER_TYPE_USER_CONTACT", 14);
+define("SYNC_FOLDER_TYPE_USER_TASK", 15);
+define("SYNC_FOLDER_TYPE_USER_JOURNAL", 16);
+define("SYNC_FOLDER_TYPE_USER_NOTE", 17);
+define("SYNC_FOLDER_TYPE_UNKNOWN", 18);
+define("SYNC_FOLDER_TYPE_RECIPIENT_CACHE", 19);
+define("SYNC_FOLDER_TYPE_DUMMY", "__dummy.Folder.Id__");
+
+define("SYNC_CONFLICT_OVERWRITE_SERVER", 0);
+define("SYNC_CONFLICT_OVERWRITE_PIM", 1);
+
+define("SYNC_TRUNCATION_HEADERS", 0);
+define("SYNC_TRUNCATION_512B", 1);
+define("SYNC_TRUNCATION_1K", 2);
+define("SYNC_TRUNCATION_5K", 4);
+define("SYNC_TRUNCATION_SEVEN", 7);
+define("SYNC_TRUNCATION_ALL", 9);
+
+define("SYNC_PROVISION_STATUS_SUCCESS", 1);
+define("SYNC_PROVISION_STATUS_PROTERROR", 2);
+define("SYNC_PROVISION_STATUS_SERVERERROR", 3);
+define("SYNC_PROVISION_STATUS_DEVEXTMANAGED", 4);
+define("SYNC_PROVISION_STATUS_POLKEYMISM", 5);
+
+define("SYNC_PROVISION_RWSTATUS_NA", 0);
+define("SYNC_PROVISION_RWSTATUS_OK", 1);
+define("SYNC_PROVISION_RWSTATUS_PENDING", 2);
+define("SYNC_PROVISION_RWSTATUS_WIPED", 3);
+
+/**
+ * Main ActiveSync class. Entry point for performing all ActiveSync operations
+ *
+ */
+class Horde_ActiveSync
+{
+    /* SYNC Status response codes */
+    const STATUS_SYNC_SUCCESS = 1;
+    const STATUS_SYNC_VERSIONMISM = 2;
+    const STATUS_SYNC_KEYMISM = 3;
+    const STATUS_SYNC_PROTERROR = 4;
+    const STATUS_SYNC_SERVERERROR = 5;
+
+    const STATUS_PING_NOCHANGES = 1;
+    const STATUS_PING_NEEDSYNC = 2;
+    const STATUS_PING_MISSING = 3;
+    const STATUS_PING_PROTERROR = 4;
+    // Hearbeat out of bounds (TODO)
+    const STATUS_PING_HBOUTOFBOUNDS = 5;
+
+    // Requested more then the max folders (TODO)
+    const STATUS_PING_MAXFOLDERS = 6;
+
+    // Folder sync is required, hierarchy out of date.
+    const STATUS_PING_FOLDERSYNCREQD = 7;
+    const STATUS_PING_SERVERERROR = 8;
+
+
+    /**
+     * DTD
+     */
+    static public $zpushdtd = array(
+                "codes" => array (
+                    0 => array (
+                        0x05 => "Synchronize",
+                        0x06 => "Replies",
+                        0x07 => "Add",
+                        0x08 => "Modify",
+                        0x09 => "Remove",
+                        0x0a => "Fetch",
+                        0x0b => "SyncKey",
+                        0x0c => "ClientEntryId",
+                        0x0d => "ServerEntryId",
+                        0x0e => "Status",
+                        0x0f => "Folder",
+                        0x10 => "FolderType",
+                        0x11 => "Version",
+                        0x12 => "FolderId",
+                        0x13 => "GetChanges",
+                        0x14 => "MoreAvailable",
+                        0x15 => "WindowSize",
+                        0x16 => "Commands",
+                        0x17 => "Options",
+                        0x18 => "FilterType",
+                        0x19 => "Truncation",
+                        0x1a => "RtfTruncation",
+                        0x1b => "Conflict",
+                        0x1c => "Folders",
+                        0x1d => "Data",
+                        0x1e => "DeletesAsMoves",
+                        0x1f => "NotifyGUID",
+                        0x20 => "Supported",
+                        0x21 => "SoftDelete",
+                        0x22 => "MIMESupport",
+                        0x23 => "MIMETruncation",
+                    ),
+                    1 => array (
+                        0x05 => "Anniversary",
+                        0x06 => "AssistantName",
+                        0x07 => "AssistnamePhoneNumber",
+                        0x08 => "Birthday",
+                        0x09 => "Body",
+                        0x0a => "BodySize",
+                        0x0b => "BodyTruncated",
+                        0x0c => "Business2PhoneNumber",
+                        0x0d => "BusinessCity",
+                        0x0e => "BusinessCountry",
+                        0x0f => "BusinessPostalCode",
+                        0x10 => "BusinessState",
+                        0x11 => "BusinessStreet",
+                        0x12 => "BusinessFaxNumber",
+                        0x13 => "BusinessPhoneNumber",
+                        0x14 => "CarPhoneNumber",
+                        0x15 => "Categories",
+                        0x16 => "Category",
+                        0x17 => "Children",
+                        0x18 => "Child",
+                        0x19 => "CompanyName",
+                        0x1a => "Department",
+                        0x1b => "Email1Address",
+                        0x1c => "Email2Address",
+                        0x1d => "Email3Address",
+                        0x1e => "FileAs",
+                        0x1f => "FirstName",
+                        0x20 => "Home2PhoneNumber",
+                        0x21 => "HomeCity",
+                        0x22 => "HomeCountry",
+                        0x23 => "HomePostalCode",
+                        0x24 => "HomeState",
+                        0x25 => "HomeStreet",
+                        0x26 => "HomeFaxNumber",
+                        0x27 => "HomePhoneNumber",
+                        0x28 => "JobTitle",
+                        0x29 => "LastName",
+                        0x2a => "MiddleName",
+                        0x2b => "MobilePhoneNumber",
+                        0x2c => "OfficeLocation",
+                        0x2d => "OtherCity",
+                        0x2e => "OtherCountry",
+                        0x2f => "OtherPostalCode",
+                        0x30 => "OtherState",
+                        0x31 => "OtherStreet",
+                        0x32 => "PagerNumber",
+                        0x33 => "RadioPhoneNumber",
+                        0x34 => "Spouse",
+                        0x35 => "Suffix",
+                        0x36 => "Title",
+                        0x37 => "WebPage",
+                        0x38 => "YomiCompanyName",
+                        0x39 => "YomiFirstName",
+                        0x3a => "YomiLastName",
+                        0x3b => "Rtf",
+                        0x3c => "Picture",
+                    ),
+                     2 => array (
+                        0x05 => "Attachment",
+                        0x06 => "Attachments",
+                        0x07 => "AttName",
+                        0x08 => "AttSize",
+                        0x09 => "AttOid",
+                        0x0a => "AttMethod",
+                        0x0b => "AttRemoved",
+                        0x0c => "Body",
+                        0x0d => "BodySize",
+                        0x0e => "BodyTruncated",
+                        0x0f => "DateReceived",
+                        0x10 => "DisplayName",
+                        0x11 => "DisplayTo",
+                        0x12 => "Importance",
+                        0x13 => "MessageClass",
+                        0x14 => "Subject",
+                        0x15 => "Read",
+                        0x16 => "To",
+                        0x17 => "Cc",
+                        0x18 => "From",
+                        0x19 => "Reply-To",
+                        0x1a => "AllDayEvent",
+                        0x1b => "Categories",
+                        0x1c => "Category",
+                        0x1d => "DtStamp",
+                        0x1e => "EndTime",
+                        0x1f => "InstanceType",
+                        0x20 => "BusyStatus",
+                        0x21 => "Location",
+                        0x22 => "MeetingRequest",
+                        0x23 => "Organizer",
+                        0x24 => "RecurrenceId",
+                        0x25 => "Reminder",
+                        0x26 => "ResponseRequested",
+                        0x27 => "Recurrences",
+                        0x28 => "Recurrence",
+                        0x29 => "Type",
+                        0x2a => "Until",
+                        0x2b => "Occurrences",
+                        0x2c => "Interval",
+                        0x2d => "DayOfWeek",
+                        0x2e => "DayOfMonth",
+                        0x2f => "WeekOfMonth",
+                        0x30 => "MonthOfYear",
+                        0x31 => "StartTime",
+                        0x32 => "Sensitivity",
+                        0x33 => "TimeZone",
+                        0x34 => "GlobalObjId",
+                        0x35 => "ThreadTopic",
+                        0x36 => "MIMEData",
+                        0x37 => "MIMETruncated",
+                        0x38 => "MIMESize",
+                        0x39 => "InternetCPID",
+                    ),
+                     3 => array (
+                        0x05 => "Notify",
+                        0x06 => "Notification",
+                        0x07 => "Version",
+                        0x08 => "Lifetime",
+                        0x09 => "DeviceInfo",
+                        0x0a => "Enable",
+                        0x0b => "Folder",
+                        0x0c => "ServerEntryId",
+                        0x0d => "DeviceAddress",
+                        0x0e => "ValidCarrierProfiles",
+                        0x0f => "CarrierProfile",
+                        0x10 => "Status",
+                        0x11 => "Replies",
+//                        0x05 => "Version='1.1'",
+                        0x12 => "Devices",
+                        0x13 => "Device",
+                        0x14 => "Id",
+                        0x15 => "Expiry",
+                        0x16 => "NotifyGUID",
+                    ),
+                     4 => array (
+                        0x05 => "Timezone",
+                        0x06 => "AllDayEvent",
+                        0x07 => "Attendees",
+                        0x08 => "Attendee",
+                        0x09 => "Email",
+                        0x0a => "Name",
+                        0x0b => "Body",
+                        0x0c => "BodyTruncated",
+                        0x0d => "BusyStatus",
+                        0x0e => "Categories",
+                        0x0f => "Category",
+                        0x10 => "Rtf",
+                        0x11 => "DtStamp",
+                        0x12 => "EndTime",
+                        0x13 => "Exception",
+                        0x14 => "Exceptions",
+                        0x15 => "Deleted",
+                        0x16 => "ExceptionStartTime",
+                        0x17 => "Location",
+                        0x18 => "MeetingStatus",
+                        0x19 => "OrganizerEmail",
+                        0x1a => "OrganizerName",
+                        0x1b => "Recurrence",
+                        0x1c => "Type",
+                        0x1d => "Until",
+                        0x1e => "Occurrences",
+                        0x1f => "Interval",
+                        0x20 => "DayOfWeek",
+                        0x21 => "DayOfMonth",
+                        0x22 => "WeekOfMonth",
+                        0x23 => "MonthOfYear",
+                        0x24 => "Reminder",
+                        0x25 => "Sensitivity",
+                        0x26 => "Subject",
+                        0x27 => "StartTime",
+                        0x28 => "UID",
+                        0x36 => "ResponseType"
+                    ), 5 => array (
+                        0x05 => "Moves",
+                        0x06 => "Move",
+                        0x07 => "SrcMsgId",
+                        0x08 => "SrcFldId",
+                        0x09 => "DstFldId",
+                        0x0a => "Response",
+                        0x0b => "Status",
+                        0x0c => "DstMsgId",
+                    ), 6 => array (
+                        0x05 => "GetItemEstimate",
+                        0x06 => "Version",
+                        0x07 => "Folders",
+                        0x08 => "Folder",
+                        0x09 => "FolderType",
+                        0x0a => "FolderId",
+                        0x0b => "DateTime",
+                        0x0c => "Estimate",
+                        0x0d => "Response",
+                        0x0e => "Status",
+                    ), 7 => array (
+                        0x05 => "Folders",
+                        0x06 => "Folder",
+                        0x07 => "DisplayName",
+                        0x08 => "ServerEntryId",
+                        0x09 => "ParentId",
+                        0x0a => "Type",
+                        0x0b => "Response",
+                        0x0c => "Status",
+                        0x0d => "ContentClass",
+                        0x0e => "Changes",
+                        0x0f => "Add",
+                        0x10 => "Remove",
+                        0x11 => "Update",
+                        0x12 => "SyncKey",
+                        0x13 => "FolderCreate",
+                        0x14 => "FolderDelete",
+                        0x15 => "FolderUpdate",
+                        0x16 => "FolderSync",
+                        0x17 => "Count",
+                        0x18 => "Version",
+                    ), 8 => array (
+                        0x05 => "CalendarId",
+                        0x06 => "FolderId",
+                        0x07 => "MeetingResponse",
+                        0x08 => "RequestId",
+                        0x09 => "Request",
+                        0x0a => "Result",
+                        0x0b => "Status",
+                        0x0c => "UserResponse",
+                        0x0d => "Version",
+                    ), 9 => array (
+                        0x05 => "Body",
+                        0x06 => "BodySize",
+                        0x07 => "BodyTruncated",
+                        0x08 => "Categories",
+                        0x09 => "Category",
+                        0x0a => "Complete",
+                        0x0b => "DateCompleted",
+                        0x0c => "DueDate",
+                        0x0d => "UtcDueDate",
+                        0x0e => "Importance",
+                        0x0f => "Recurrence",
+                        0x10 => "Type",
+                        0x11 => "Start",
+                        0x12 => "Until",
+                        0x13 => "Occurrences",
+                        0x14 => "Interval",
+                        0x16 => "DayOfWeek",
+                        0x15 => "DayOfMonth",
+                        0x17 => "WeekOfMonth",
+                        0x18 => "MonthOfYear",
+                        0x19 => "Regenerate",
+                        0x1a => "DeadOccur",
+                        0x1b => "ReminderSet",
+                        0x1c => "ReminderTime",
+                        0x1d => "Sensitivity",
+                        0x1e => "StartDate",
+                        0x1f => "UtcStartDate",
+                        0x20 => "Subject",
+                        0x21 => "Rtf",
+                    ), 0xa => array (
+                        0x05 => "ResolveRecipients",
+                        0x06 => "Response",
+                        0x07 => "Status",
+                        0x08 => "Type",
+                        0x09 => "Recipient",
+                        0x0a => "DisplayName",
+                        0x0b => "EmailAddress",
+                        0x0c => "Certificates",
+                        0x0d => "Certificate",
+                        0x0e => "MiniCertificate",
+                        0x0f => "Options",
+                        0x10 => "To",
+                        0x11 => "CertificateRetrieval",
+                        0x12 => "RecipientCount",
+                        0x13 => "MaxCertificates",
+                        0x14 => "MaxAmbiguousRecipients",
+                        0x15 => "CertificateCount",
+                    ), 0xb => array (
+                        0x05 => "ValidateCert",
+                        0x06 => "Certificates",
+                        0x07 => "Certificate",
+                        0x08 => "CertificateChain",
+                        0x09 => "CheckCRL",
+                        0x0a => "Status",
+                    ), 0xc => array (
+                        0x05 => "CustomerId",
+                        0x06 => "GovernmentId",
+                        0x07 => "IMAddress",
+                        0x08 => "IMAddress2",
+                        0x09 => "IMAddress3",
+                        0x0a => "ManagerName",
+                        0x0b => "CompanyMainPhone",
+                        0x0c => "AccountName",
+                        0x0d => "NickName",
+                        0x0e => "MMS",
+                    ), 0xd => array (
+                        0x05 => "Ping",
+                        0x07 => "Status",
+                        0x08 => "LifeTime",
+                        0x09 => "Folders",
+                        0x0a => "Folder",
+                        0x0b => "ServerEntryId",
+                        0x0c => "FolderType",
+                    ), 0xe => array (
+                        0x05 => "Provision",
+                        0x06 => "Policies",
+                        0x07 => "Policy",
+                        0x08 => "PolicyType",
+                        0x09 => "PolicyKey",
+                        0x0A => "Data",
+                        0x0B => "Status",
+                        0x0C => "RemoteWipe",
+                        0x0D => "EASProvisionDoc",
+                        ),
+                    0xf => array(
+                        0x05 => "Search",
+                        0x07 => "Store",
+                        0x08 => "Name",
+                        0x09 => "Query",
+                        0x0A => "Options",
+                        0x0B => "Range",
+                        0x0C => "Status",
+                        0x0D => "Response",
+                        0x0E => "Result",
+                        0x0F => "Properties",
+                        0x10 => "Total",
+                        0x11 => "EqualTo",
+                        0x12 => "Value",
+                        0x13 => "And",
+                        0x14 => "Or",
+                        0x15 => "FreeText",
+                        0x17 => "DeepTraversal",
+                        0x18 => "LongId",
+                        0x19 => "RebuildResults",
+                        0x1A => "LessThan",
+                        0x1B => "GreaterThan",
+                        0x1C => "Schema",
+                        0x1D => "Supported",
+                    ), 0x10 => array(
+                        0x05 => "DisplayName",
+                        0x06 => "Phone",
+                        0x07 => "Office",
+                        0x08 => "Title",
+                        0x09 => "Company",
+                        0x0A => "Alias",
+                        0x0B => "FirstName",
+                        0x0C => "LastName",
+                        0x0D => "HomePhone",
+                        0x0E => "MobilePhone",
+                        0x0F => "EmailAddress",
+                    )
+              ), "namespaces" => array(
+                  1 => "POOMCONTACTS",
+                  2 => "POOMMAIL",
+                  3 => "AirNotify",
+                  4 => "POOMCAL",
+                  5 => "Move",
+                  6 => "GetItemEstimate",
+                  7 => "FolderHierarchy",
+                  8 => "MeetingResponse",
+                  9 => "POOMTASKS",
+                  0xA => "ResolveRecipients",
+                  0xB => "ValidateCerts",
+                  0xC => "POOMCONTACTS2",
+                  0xD => "Ping",
+                  0xE => "Provision",//
+                  0xF => "Search",//
+                  0x10 => "GAL",
+              )
+          );
+
+    /**
+     * Used to track what error code to send back to PIM on failure
+     *
+     * @var integer
+     */
+    protected $_statusCode = 0;
+
+    protected $_provisioning;
+
+    /**
+     * Const'r
+     *
+     * @param Horde_ActiveSync_Driver $driver            The backend driver
+     * @param Horde_ActiveSync_StateMachine $state       The state machine
+     * @param Horde_ActiveSync_Wbxml_Decoder $decoder    The Wbxml decoder
+     * @param Horde_ActiveSync_Wbxml_Endcodder $encdoer  The Wbxml encoder
+     *
+     * @return Horde_ActiveSync
+     */
+    public function __construct(Horde_ActiveSync_Driver_Base $driver,
+                                Horde_ActiveSync_Wbxml_Decoder $decoder,
+                                Horde_ActiveSync_Wbxml_Encoder $encoder,
+                                Horde_Controller_Request_Http $request)
+    {
+        /* Backend driver */
+        $this->_driver = $driver;
+
+        /* Wbxml handlers */
+        $this->_encoder = $encoder;
+        $this->_decoder = $decoder;
+
+        /* The http request */
+        $this->_request = $request;
+    }
+
+    /**
+     * Setter for the logger
+     *
+     * @param Horde_Log_Logger $logger  The logger object.
+     *
+     * @return void
+     */
+    public function setLogger(Horde_Log_Logger $logger)
+    {
+        $this->_logger = $logger;
+        $this->_encoder->setLogger($logger);
+        $this->_decoder->setLogger($logger);
+        $this->_driver->setLogger($logger);
+    }
+
+    /**
+     * Setter for provisioning support
+     *
+     */
+    public function setProvisioning($provision)
+    {
+        $this->_provisioning = $provision;
+    }
+
+    /**
+     *
+     * @param $protocolversion
+     *
+     * @return true
+     */
+    public function handleMoveItems($protocolversion)
+    {
+        if (!$this->_decoder->getElementStartTag(SYNC_MOVE_MOVES)) {
+            return false;
+        }
+
+        $moves = array();
+        while ($this->_decoder->getElementStartTag(SYNC_MOVE_MOVE)) {
+            $move = array();
+            if ($this->_decoder->getElementStartTag(SYNC_MOVE_SRCMSGID)) {
+                $move['srcmsgid'] = $this->_decoder->getElementContent();
+                if(!$this->_decoder->getElementEndTag())
+                    break;
+            }
+            if ($this->_decoder->getElementStartTag(SYNC_MOVE_SRCFLDID)) {
+                $move['srcfldid'] = $this->_decoder->getElementContent();
+                if (!$this->_decoder->getElementEndTag()) {
+                    break;
+                }
+            }
+            if ($this->_decoder->getElementStartTag(SYNC_MOVE_DSTFLDID)) {
+                $move['dstfldid'] = $this->_decoder->getElementContent();
+                if (!$this->_decoder->getElementEndTag()) {
+                    break;
+                }
+            }
+            array_push($moves, $move);
+
+            if (!$this->_decoder->getElementEndTag()) {
+                return false;
+            }
+        }
+
+        if (!$this->_decoder->getElementEndTag())
+            return false;
+
+        $this->_encoder->StartWBXML();
+
+        $this->_encoder->startTag(SYNC_MOVE_MOVES);
+
+        foreach ($moves as $move) {
+            $this->_encoder->startTag(SYNC_MOVE_RESPONSE);
+            $this->_encoder->startTag(SYNC_MOVE_SRCMSGID);
+            $this->_encoder->content($move['srcmsgid']);
+            $this->_encoder->endTag();
+
+            $importer = $this->_driver->GetContentsImporter($move['srcfldid']);
+            $result = $importer->ImportMessageMove($move['srcmsgid'], $move['dstfldid']);
+
+            // We discard the importer state for now.
+            $this->_encoder->startTag(SYNC_MOVE_STATUS);
+            $this->_encoder->content($result ? 3 : 1);
+            $this->_encoder->endTag();
+
+            $this->_encoder->startTag(SYNC_MOVE_DSTMSGID);
+            $this->_encoder->content(is_string($result) ? $result : $move['srcmsgid']);
+            $this->_encoder->endTag();
+            $this->_encoder->endTzg();
+        }
+        $this->_encoder->endTag();
+
+        return true;
+    }
+
+    /**
+     * @param $protocolversion
+     *
+     * @return boolean
+     */
+    public function handleNotify($protocolversion)
+    {
+        if (!$this->_decoder->getElementStartTag(SYNC_AIRNOTIFY_NOTIFY)) {
+            return false;
+        }
+
+        if (!$this->_decoder->getElementStartTag(SYNC_AIRNOTIFY_DEVICEINFO)) {
+            return false;
+        }
+
+        if (!$this->_decoder->getElementEndTag()) {
+            return false;
+        }
+
+        if (!$this->_decoder->getElementEndTag()) {
+            return false;
+        }
+        $this->_encoder->StartWBXML();
+        $this->_encoder->startTag(SYNC_AIRNOTIFY_NOTIFY);
+        $this->_encoder->startTag(SYNC_AIRNOTIFY_STATUS);
+        $this->_encoder->content(1);
+        $this->_encoder->endTag();
+        $this->_encoder->startTag(SYNC_AIRNOTIFY_VALIDCARRIERPROFILES);
+        $this->_encoder->endTag();
+        $this->_encoder->endTag();
+
+        return true;
+    }
+
+    /**
+     * handle GetHierarchy method - simply returns current hierarchy of all
+     * folders
+     *
+     * @param string $protocolversion
+     * @param string $devid
+     *
+     * @return boolean
+     */
+    public function handleGetHierarchy($protocolversion, $devid)
+    {
+        $folders = $this->_driver->GetHierarchy();
+        if (!$folders) {
+            return false;
+        }
+
+        // save folder-ids for fourther syncing
+        $this->_stateMachine->setFolderData($devid, $folders);
+
+        $this->_encoder->StartWBXML();
+        $this->_encoder->startTag(SYNC_FOLDERHIERARCHY_FOLDERS);
+
+        foreach ($folders as $folder) {
+            $this->_encoder->startTag(SYNC_FOLDERHIERARCHY_FOLDER);
+            $folder->encodeStream($this->_encoder);
+            $this->_encoder->endTag();
+        }
+        $this->_encoder->endTag();
+
+        return true;
+    }
+
+    /**
+     *
+     * @param $protocolversion
+     * @param $devid
+     * @return unknown_type
+     */
+    public function handleGetItemEstimate($protocolversion, $devid)
+    {
+        $collections = array();
+
+        if (!$this->_decoder->getElementStartTag(SYNC_GETITEMESTIMATE_GETITEMESTIMATE)) {
+            return false;
+        }
+
+        if (!$this->_decoder->getElementStartTag(SYNC_GETITEMESTIMATE_FOLDERS)) {
+            return false;
+        }
+
+        while ($this->_decoder->getElementStartTag(SYNC_GETITEMESTIMATE_FOLDER)) {
+            $collection = array();
+
+            if (!$this->_decoder->getElementStartTag(SYNC_GETITEMESTIMATE_FOLDERTYPE)) {
+                return false;
+            }
+
+            $class = $this->_decoder->getElementContent();
+
+            if (!$this->_decoder->getElementEndTag()) {
+                return false;
+            }
+
+            if ($this->_decoder->getElementStartTag(SYNC_GETITEMESTIMATE_FOLDERID)) {
+                $collectionid = $this->_decoder->getElementContent();
+
+                if (!$this->_decoder->getElementEndTag()) {
+                    return false;
+                }
+            }
+
+            if (!$this->_decoder->getElementStartTag(SYNC_FILTERTYPE)) {
+                return false;
+            }
+            $filtertype = $this->_decoder->getElementContent();
+
+            if (!$this->_decoder->getElementEndTag()) {
+                return false;
+            }
+
+            if (!$this->_decoder->getElementStartTag(SYNC_SYNCKEY)) {
+                return false;
+            }
+
+            $synckey = $this->_decoder->getElementContent();
+
+            if (!$this->_decoder->getElementEndTag()) {
+                return false;
+            }
+            if (!$this->_decoder->getElementEndTag()) {
+                return false;
+            }
+
+            // compatibility mode - get folderid from the state directory
+            if (!isset($collectionid)) {
+                $collectionid = $this->_stateMachine->getFolderData($devid, $class);
+            }
+
+            $collection = array();
+            $collection['synckey'] = $synckey;
+            $collection['class'] = $class;
+            $collection['filtertype'] = $filtertype;
+            $collection['collectionid'] = $collectionid;
+
+            array_push($collections, $collection);
+        }
+
+        $this->_encoder->startWBXML();
+
+        $this->_encoder->startTag(SYNC_GETITEMESTIMATE_GETITEMESTIMATE);
+        foreach ($collections as $collection) {
+            $this->_encoder->startTag(SYNC_GETITEMESTIMATE_RESPONSE);
+
+            $this->_encoder->startTag(SYNC_GETITEMESTIMATE_STATUS);
+            $this->_encoder->content(1);
+            $this->_encoder->endTag();
+
+            $this->_encoder->startTag(SYNC_GETITEMESTIMATE_FOLDER);
+
+            $this->_encoder->startTag(SYNC_GETITEMESTIMATE_FOLDERTYPE);
+            $this->_encoder->content($collection['class']);
+            $this->_encoder->endTag();
+
+            $this->_encoder->startTag(SYNC_GETITEMESTIMATE_FOLDERID);
+            $this->_encoder->content($collection['collectionid']);
+            $this->_encoder->endTag();
+
+            $this->_encoder->startTag(SYNC_GETITEMESTIMATE_ESTIMATE);
+
+            $importer = new Horde_ActiveSync_ContentsCache();
+
+            $syncstate = $this->_stateMachine->loadState($collection['synckey']);
+
+            $exporter = $this->_driver->GetExporter($collection['collectionid']);
+            $exporter->Config($importer, $collection['class'], $collection['filtertype'], $syncstate, 0, 0);
+
+            $this->_encoder->content($exporter->GetChangeCount());
+
+            $this->_encoder->endTag();
+
+            $this->_encoder->endTag();
+
+            $this->_encoder->endTag();
+        }
+
+        $this->_encoder->endTag();
+
+        return true;
+    }
+
+    /**
+     * @param $protocolversion
+     * @return unknown_type
+     */
+    public function handleGetAttachment($protocolversion)
+    {
+        $get = $this->_request->getGetParams();
+        $attname = $get('AttachmentName');
+        if (!isset($attname)) {
+            return false;
+        }
+
+        header("Content-Type: application/octet-stream");
+        $this->_driver->GetAttachmentData($attname);
+
+        return true;
+    }
+
+    /**
+     *
+     * @param $protocolversion
+     * @return unknown_type
+     */
+    public function handleSendMail($protocolversion)
+    {
+        // All that happens here is that we receive an rfc822 message on stdin
+        // and just forward it to the backend. We provide no output except for
+        // an OK http reply
+        $rfc822 = $this->readStream();
+
+        return $this->_driver->SendMail($rfc822);
+    }
+
+    /**
+     *
+     * @param $protocolversion
+     * @return unknown_type
+     */
+    public function handleSmartForward($protocolversion)
+    {
+        // SmartForward is a normal 'send' except that you should attach the
+        // original message which is specified in the URL
+
+        $rfc822 = $this->readStream();
+
+        if (isset($_GET["ItemId"])) {
+            $orig = $_GET["ItemId"];
+        } else {
+            $orig = false;
+        }
+        if (isset($_GET["CollectionId"])) {
+            $parent = $_GET["CollectionId"];
+        } else {
+            $parent = false;
+        }
+
+        return $this->_driver->SendMail($rfc822, $orig, false, $parent);
+    }
+
+    /**
+     * @TODO: use Horde_Controller_Request_Http for the GET
+     *
+     * @param unknown_type $protocolversion
+     * @return unknown_type
+     */
+    public function handleSmartReply($protocolversion)
+    {
+        // Smart reply should add the original message to the end of the message body
+        $rfc822 = $this->readStream();
+
+        if (isset($_GET["ItemId"])) {
+            $orig = $_GET["ItemId"];
+        } else {
+            $orig = false;
+        }
+
+        if (isset($_GET["CollectionId"])) {
+            $parent = $_GET["CollectionId"];
+        } else {
+            $parent = false;
+        }
+
+        return $this->_driver->SendMail($rfc822, false, $orig, $parent);
+    }
+
+    /**
+     * @param $protocolversion
+     * @return unknown_type
+     */
+    public function handleFolderCreate($protocolversion)
+    {
+        $el = $this->_decoder->getElement();
+        if ($el[Horde_ActiveSync_Wbxml::EN_TYPE] != Horde_ActiveSync_Wbxml::EN_TYPE_STARTTAG) {
+            return false;
+        }
+
+        $create = $update = $delete = false;
+
+        if ($el[Horde_ActiveSync_Wbxml::EN_TAG] == SYNC_FOLDERHIERARCHY_FOLDERCREATE) {
+            $create = true;
+        } elseif ($el[Horde_ActiveSync_Wbxml::EN_TAG] == SYNC_FOLDERHIERARCHY_FOLDERUPDATE) {
+            $update = true;
+        } elseif ($el[Horde_ActiveSync_Wbxml::EN_TAG] == SYNC_FOLDERHIERARCHY_FOLDERDELETE) {
+            $delete = true;
+        }
+
+        if (!$create && !$update && !$delete) {
+            return false;
+        }
+
+        // SyncKey
+        if (!$this->_decoder->getElementStartTag(SYNC_FOLDERHIERARCHY_SYNCKEY)) {
+            return false;
+        }
+        $synckey = $this->_decoder->getElementContent();
+        if (!$this->_decoder->getElementEndTag()) {
+            return false;
+        }
+
+        // ServerID
+        $serverid = false;
+        if ($this->_decoder->getElementStartTag(SYNC_FOLDERHIERARCHY_SERVERENTRYID)) {
+            $serverid = $this->_decoder->getElementContent();
+            if (!$this->_decoder->getElementEndTag()) {
+                return false;
+            }
+        }
+
+        // when creating or updating more information is necessary
+        if (!$delete) {
+            // Parent
+            $parentid = false;
+            if ($this->_decoder->getElementStartTag(SYNC_FOLDERHIERARCHY_PARENTID)) {
+                $parentid = $this->_decoder->getElementContent();
+                if (!$this->_decoder->getElementEndTag()) {
+                    return false;
+                }
+            }
+
+            // Displayname
+            if (!$this->_decoder->getElementStartTag(SYNC_FOLDERHIERARCHY_DISPLAYNAME)) {
+                return false;
+            }
+            $displayname = $this->_decoder->getElementContent();
+            if (!$this->_decoder->getElementEndTag()) {
+                return false;
+            }
+
+            // Type
+            $type = false;
+            if ($this->_decoder->getElementStartTag(SYNC_FOLDERHIERARCHY_TYPE)) {
+                $type = $this->_decoder->getElementContent();
+                if (!$this->_decoder->getElementEndTag()) {
+                    return false;
+                }
+            }
+        }
+
+        if (!$this->_decoder->getElementEndTag()) {
+            return false;
+        }
+
+        // Get state of hierarchy
+        $syncstate = $this->_stateMachine->loadState($synckey);
+        $newsynckey = $this->_stateMachine->getNewSyncKey($synckey);
+
+        // additional information about already seen folders
+        $seenfolders = unserialize($this->_stateMachine->loadState('s' . $synckey));
+        if (!$seenfolders) {
+            $seenfolders = array();
+        }
+        // Configure importer with last state
+        $importer = $this->_driver->GetHierarchyImporter();
+        $importer->Config($syncstate);
+
+        if (!$delete) {
+            // Send change
+            $serverid = $importer->ImportFolderChange($serverid, $parentid, $displayname, $type);
+        } else {
+            // delete folder
+            $deletedstat = $importer->ImportFolderDeletion($serverid, 0);
+        }
+
+        $this->_encoder->startWBXML();
+        if ($create) {
+            // add folder id to the seen folders
+            $seenfolders[] = $serverid;
+
+            $this->_encoder->startTag(SYNC_FOLDERHIERARCHY_FOLDERCREATE);
+
+
+            $this->_encoder->startTag(SYNC_FOLDERHIERARCHY_STATUS);
+            $this->_encoder->content(1);
+            $this->_encoder->endTag();
+
+            $this->_encoder->startTag(SYNC_FOLDERHIERARCHY_SYNCKEY);
+            $this->_encoder->content($newsynckey);
+            $this->_encoder->endTag();
+
+            $this->_encoder->startTag(SYNC_FOLDERHIERARCHY_SERVERENTRYID);
+            $this->_encoder->content($serverid);
+            $this->_encoder->endTag();
+
+            $this->_encoder->endTag();
+
+            $this->_encoder->endTag();
+        } elseif ($update) {
+
+            $this->_encoder->startTag(SYNC_FOLDERHIERARCHY_FOLDERUPDATE);
+
+            $this->_encoder->startTag(SYNC_FOLDERHIERARCHY_STATUS);
+            $this->_encoder->content(1);
+            $this->_encoder->endTag();
+
+            $this->_encoder->startTag(SYNC_FOLDERHIERARCHY_SYNCKEY);
+            $this->_encoder->content($newsynckey);
+            $this->_encoder->endTag();
+
+            $this->_encoder->endTag();
+        } elseif ($delete) {
+            $this->_encoder->startTag(SYNC_FOLDERHIERARCHY_FOLDERDELETE);
+
+            $this->_encoder->startTag(SYNC_FOLDERHIERARCHY_STATUS);
+            $this->_encoder->content($deletedstat);
+            $this->_encoder->endTag();
+
+            $this->_encoder->startTag(SYNC_FOLDERHIERARCHY_SYNCKEY);
+            $this->_encoder->content($newsynckey);
+            $this->_encoder->endTag();
+
+            $this->_encoder->endTag();
+
+            // remove folder from the folderflags array
+            if (($sid = array_search($serverid, $seenfolders)) !== false) {
+                unset($seenfolders[$sid]);
+                $seenfolders = array_values($seenfolders);
+                $this->_logger->debug('Deleted from seenfolders: ' . $serverid);
+            }
+        }
+
+        $this->_encoder->endTag();
+        // Save the sync state for the next time
+        $this->_stateMachine->setState($newsynckey, $importer->GetState());
+        $this->_stateMachine->setState('s' . $newsynckey, serialize($seenfolders));
+        $this->_stateMachine->save();
+
+        return true;
+    }
+
+    /**
+     * handle meetingresponse method
+     */
+    public function handleMeetingResponse($protocolversion)
+    {
+        $requests = Array();
+        if (!$this->_decoder->getElementStartTag(SYNC_MEETINGRESPONSE_MEETINGRESPONSE)) {
+            return false;
+        }
+
+        while ($this->_decoder->getElementStartTag(SYNC_MEETINGRESPONSE_REQUEST)) {
+            $req = Array();
+
+            if ($this->_decoder->getElementStartTag(SYNC_MEETINGRESPONSE_USERRESPONSE)) {
+                $req['response'] = $this->_decoder->getElementContent();
+                if (!$this->_decoder->getElementEndTag()) {
+                    return false;
+                }
+            }
+
+            if ($this->_decoder->getElementStartTag(SYNC_MEETINGRESPONSE_FOLDERID)) {
+                $req['folderid'] = $this->_decoder->getElementContent();
+                if (!$this->_decoder->getElementEndTag()) {
+                    return false;
+                }
+            }
+
+            if ($this->_decoder->getElementStartTag(SYNC_MEETINGRESPONSE_REQUESTID)) {
+                $req['requestid'] = $this->_decoder->getElementContent();
+                if (!$this->_decoder->getElementEndTag()) {
+                    return false;
+                }
+            }
+
+            if (!$this->_decoder->getElementEndTag()) {
+                return false;
+            }
+
+            array_push($requests, $req);
+        }
+
+        if (!$this->_decoder->getElementEndTag()) {
+            return false;
+        }
+
+        // Start output, simply the error code, plus the ID of the calendar item that was generated by the
+        // accept of the meeting response
+        $this->_encoder->StartWBXML();
+        $this->_encoder->startTag(SYNC_MEETINGRESPONSE_MEETINGRESPONSE);
+
+        foreach ($requests as $req) {
+            $calendarid = '';
+            $ok = $this->_driver->MeetingResponse($req['requestid'], $req['folderid'], $req['response'], $calendarid);
+            $this->_encoder->startTag(SYNC_MEETINGRESPONSE_RESULT);
+            $this->_encoder->startTag(SYNC_MEETINGRESPONSE_REQUESTID);
+            $this->_encoder->content($req['requestid']);
+            $this->_encoder->endTag();
+            $this->_encoder->startTag(SYNC_MEETINGRESPONSE_STATUS);
+            $this->_encoder->content($ok ? 1 : 2);
+            $this->_encoder->endTag();
+            if ($ok) {
+                $this->_encoder->startTag(SYNC_MEETINGRESPONSE_CALENDARID);
+                $this->_encoder->content($calendarid);
+                $this->_encoder->endTag();
+            }
+            $this->_encoder->endTag();
+        }
+
+        $this->_encoder->endTag();
+
+        return true;
+    }
+
+
+    /**
+     * @param $protocolversion
+     * @return unknown_type
+     */
+    public function handleFolderUpdate($protocolversion)
+    {
+        return $this->handleFolderCreate($protocolversion);
+    }
+
+    /**
+     *
+     * @param $protocolversion
+     * @return unknown_type
+     */
+    public function handleFolderDelete($protocolversion) {
+        return $this->handleFolderCreate($this->_driver, $protocolversion);
+    }
+
+    public function provisioningRequired()
+    {
+        self::provisionHeader();
+        self::activeSyncHeader();
+        self::versionHeader();
+        self::commandsHeader();
+        header("Cache-Control: private");
+    }
+
+    /**
+     * @param $devid
+     * @param $protocolversion
+     * @return unknown_type
+     */
+    public function handleSearch($devid, $protocolversion)
+    {
+        $searchrange = '0';
+        if (!$this->_decoder->getElementStartTag(SYNC_SEARCH_SEARCH)) {
+            return false;
+        }
+
+        if (!$this->_decoder->getElementStartTag(SYNC_SEARCH_STORE)) {
+            return false;
+        }
+
+        if (!$this->_decoder->getElementStartTag(SYNC_SEARCH_NAME)) {
+            return false;
+        }
+        $searchname = $this->_decoder->getElementContent();
+        if (!$this->_decoder->getElementEndTag()) {
+            return false;
+        }
+
+        if (!$this->_decoder->getElementStartTag(SYNC_SEARCH_QUERY)) {
+            return false;
+        }
+        $searchquery = $this->_decoder->getElementContent();
+        if (!$this->_decoder->getElementEndTag()) {
+            return false;
+        }
+
+        if ($this->_decoder->getElementStartTag(SYNC_SEARCH_OPTIONS)) {
+            while(1) {
+                if ($this->_decoder->getElementStartTag(SYNC_SEARCH_RANGE)) {
+                    $searchrange = $this->_decoder->getElementContent();
+                    if (!$this->_decoder->getElementEndTag()) {
+                        return false;
+                    }
+                }
+                $e = $this->_decoder->peek();
+                if ($e[Horde_ActiveSync_Wbxml::EN_TYPE] == Horde_ActiveSync_Wbxml::EN_TYPE_ENDTAG) {
+                    $this->_decoder->getElementEndTag();
+                    break;
+                }
+            }
+        }
+        if (!$this->_decoder->getElementEndTag()) {//store
+            return false;
+        }
+
+        if (!$this->_decoder->getElementEndTag()) {//search
+            return false;
+        }
+
+        if (strtoupper($searchname) != "GAL") {
+            $this->_logger->err('Searchtype ' . $searchname . 'is not supported');
+            return false;
+        }
+        //get search results from backend
+        $rows = $this->_driver->getSearchResults($searchquery, $searchrange);
+
+        $this->_encoder->startWBXML();
+        $this->_encoder->startTag(SYNC_SEARCH_SEARCH);
+
+            $this->_encoder->startTag(SYNC_SEARCH_STATUS);
+            $this->_encoder->content(1);
+            $this->_encoder->endTag();
+
+            $this->_encoder->startTag(SYNC_SEARCH_RESPONSE);
+                $this->_encoder->startTag(SYNC_SEARCH_STORE);
+
+                    $this->_encoder->startTag(SYNC_SEARCH_STATUS);
+                    $this->_encoder->content(1);
+                    $this->_encoder->endTag();
+
+                    if (is_array($rows) && !empty($rows)) {
+                        $searchrange = $rows['range'];
+                        unset($rows['range']);
+                        foreach ($rows as $u) {
+                            $this->_encoder->startTag(SYNC_SEARCH_RESULT);
+                                $this->_encoder->startTag(SYNC_SEARCH_PROPERTIES);
+
+                                    $this->_encoder->startTag(SYNC_GAL_DISPLAYNAME);
+                                    $this->_encoder->content($u["fullname"]);
+                                    $this->_encoder->endTag();
+
+                                    $this->_encoder->startTag(SYNC_GAL_PHONE);
+                                    $this->_encoder->content($u["businessphone"]);
+                                    $this->_encoder->endTag();
+
+                                    $this->_encoder->startTag(SYNC_GAL_ALIAS);
+                                    $this->_encoder->content($u["username"]);
+                                    $this->_encoder->endTag();
+
+                                    //it's not possible not get first and last name of an user
+                                    //from the gab and user functions, so we just set fullname
+                                    //to lastname and leave firstname empty because nokia needs
+                                    //first and lastname in order to display the search result
+                                    $this->_encoder->startTag(SYNC_GAL_FIRSTNAME);
+                                    $this->_encoder->content("");
+                                    $this->_encoder->endTag();
+
+                                    $this->_encoder->startTag(SYNC_GAL_LASTNAME);
+                                    $this->_encoder->content($u["fullname"]);
+                                    $this->_encoder->endTag();
+
+                                    $this->_encoder->startTag(SYNC_GAL_EMAILADDRESS);
+                                    $this->_encoder->content($u["emailaddress"]);
+                                    $this->_encoder->endTag();
+
+                                $this->_encoder->endTag();//result
+                            $this->_encoder->endTag();//properties
+                        }
+                        $this->_encoder->startTag(SYNC_SEARCH_RANGE);
+                        $this->_encoder->content($searchrange);
+                        $this->_encoder->endTag();
+
+                        $this->_encoder->startTag(SYNC_SEARCH_TOTAL);
+                        $this->_encoder->content(count($rows));
+                        $this->_encoder->endTag();
+                    }
+
+                $this->_encoder->endTag();//store
+            $this->_encoder->endTag();//response
+        $this->_encoder->endTag();//search
+
+
+        return true;
+    }
+
+    /**
+     * @param $cmd
+     * @param $devid
+     * @param $protocolversion
+     * @return unknown_type
+     */
+    public function handleRequest($cmd, $devId, $version)
+    {
+        $class = 'Horde_ActiveSync_Request_' . basename($cmd);
+        if (class_exists($class)) {
+            $request = new $class($this->_driver,
+                                  $this->_decoder,
+                                  $this->_encoder,
+                                  $this->_request,
+                                  $version,
+                                  $devId,
+                                  $this->_provisioning);
+            $request->setLogger($this->_logger);
+            return $request->handle($this);
+        }
+
+        // GetHierarchy is used in v1.0 of the AS protocol, in v2, it is replaced
+        // by the FolderSync command
+
+        // @TODO: Leave the following in place until all are refactored...then throw
+        // an error if the class does not exist.
+        switch($cmd) {
+            case 'SendMail':
+                $status = $this->handleSendMail($version);
+                break;
+            case 'SmartForward':
+                $status = $this->handleSmartForward($version);
+                break;
+            case 'SmartReply':
+                $status = $this->handleSmartReply($version);
+                break;
+            case 'GetAttachment':
+                $status = $this->handleGetAttachment($version);
+                break;
+            case 'GetHierarchy':
+                $status = $this->handleGetHierarchy($version, $devId);
+                break;
+            case 'CreateCollection':
+                $status = $this->handleCreateCollection($version);
+                break;
+            case 'DeleteCollection':
+                $status = $this->handleDeleteCollection($version);
+                break;
+            case 'MoveCollection':
+                $status = $this->handleMoveCollection($version);
+                break;
+            case 'FolderCreate':
+                $status = $this->handleFolderCreate($version);
+                break;
+            case 'FolderDelete':
+                $status = $this->handleFolderDelete($version);
+                break;
+            case 'FolderUpdate':
+                $status = $this->handleFolderUpdate($version);
+                break;
+            case 'MoveItems':
+                $status = $this->handleMoveItems($version);
+                break;
+            case 'GetItemEstimate':
+                $status = $this->handleGetItemEstimate($version, $devId);
+                break;
+            case 'MeetingResponse':
+                $status = $this->handleMeetingResponse($version);
+                break;
+            case 'Notify': // Used for sms-based notifications (pushmail)
+                $status = $this->handleNotify($version);
+                break;
+            case 'Search':
+                $status = $this->handleSearch($devId, $version);
+                break;
+
+            default:
+                $this->_logger->err('Unknown command - not implemented');
+                $status = false;
+                break;
+        }
+
+        return $status;
+    }
+
+    /**
+     * Read input from the php input stream
+     *
+     * @TODO: Get rid of this - the wbxml classes have a php:// stream already
+     *        and when we need *just* the stream and not wbxml, we can use
+     *        $request->body
+     *
+     * @return string
+     */
+    public function readStream()
+    {
+        $s = "";
+        while (1) {
+            $data = fread($this->_inputStream, 4096);
+            if (strlen($data) == 0) {
+                break;
+            }
+            $s .= $data;
+        }
+
+        return $s;
+    }
+
+    /**
+     * Send the MS_Server-ActiveSync header
+     *
+     * @return void
+     */
+    static public function activeSyncHeader()
+    {
+        header("MS-Server-ActiveSync: 6.5.7638.1");
+    }
+
+    /**
+     * Send the protocol versions header
+     *
+     * @return void
+     */
+    static public function versionHeader()
+    {
+        header("MS-ASProtocolVersions: 1.0,2.0,2.1,2.5");
+    }
+
+    /**
+     * send protocol commands header
+     *
+     * @return void
+     */
+    static public function commandsHeader()
+    {
+        header("MS-ASProtocolCommands: Sync,SendMail,SmartForward,SmartReply,GetAttachment,GetHierarchy,CreateCollection,DeleteCollection,MoveCollection,FolderSync,FolderCreate,FolderDelete,FolderUpdate,MoveItems,GetItemEstimate,MeetingResponse,ResolveRecipients,ValidateCert,Provision,Search,Ping");
+    }
+
+    /**
+     * Send provision header
+     *
+     * @return void
+     */
+    static public function provisionHeader()
+    {
+        header("HTTP/1.1 449 Retry after sending a PROVISION command");
+    }
+
+    /**
+     * Obtain the policy key header from the request.
+     *
+     * @return int  The policy key or zero if not set.
+     */
+    public function getPolicyKey()
+    {
+        if (isset($this->_policykey)) {
+            return $this->_policykey;
+        }
+
+        /* Policy key headers may be sent in either of these forms: */
+        $this->_policykey = $this->_request->getHeader('X-Ms-Policykey');
+        if (empty($this->_policykey)) {
+            $this->_policykey = $this->_request->getHeader('X-MS-PolicyKey');
+        }
+        if (empty($this->_policykey)) {
+            $this->_policykey = 0;
+        }
+
+        return $this->_policykey;
+    }
+
+    /**
+     * Obtain the ActiveSync protocol version
+     */
+    public function getProtocolVersion()
+    {
+        if (isset($this->_version)) {
+            return $this->_version;
+        }
+
+        $this->_version = $this->_request->getHeader('Ms-Asprotocolversion');
+        if (empty($this->_version)) {
+            $this->_version = $this->_request->getHeader('MS-ASProtocolVersion');
+        }
+        if (empty($this->_version)) {
+            $this->_version = '1.0';
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/ActiveSync/Factory.php b/framework/ActiveSync/lib/Horde/ActiveSync/ActiveSync/Factory.php
new file mode 100644 (file)
index 0000000..9f44b08
--- /dev/null
@@ -0,0 +1,33 @@
+<?php
+/* 
+ * To change this template, choose Tools | Templates
+ * and open the template in the editor.
+ */
+
+/**
+ * The Autoloader allows us to omit "require/include" statements.
+ */
+require_once 'Horde/Autoloader.php';
+
+class Horde_ActiveSync_Factory
+{
+
+    static public function getActiveSync(Horde_Injector $injector)
+    {
+        // @TODO: For now, just drop all this in here, need to create factories
+        // for lots of these dependencies. Also, some of these (like registry
+        // and request might already be bound to the injector).
+        $registry = new Horde_Registry();
+        $connector = new Horde_ActiveSync_Driver_Horde_Connector_Registry(array('registry' => $registry));
+        $driver = new Horde_ActiveSync_Driver_Horde(array('connectory' => $connector));
+        $state = new Horde_ActiveSync_StateMachine_File(array('stateDir' => '/tmp'));
+        $encoder = new Horde_ActiveSync_Wbxml_Encoder(fopen("php://output", "w+"),
+                                                      Horde_ActiveSync::$zpushdtd);
+        $decoder = new Horde_ActiveSync_Wbxml_Decoder(fopen('php://input', 'r'),
+                                                      Horde_ActiveSync::$zpushdtd);
+
+        $request = new Horde_Controller_Request_Http();
+        $server = new Horde_ActiveSync($driver, $state, $decoder, $encoder, $request);
+    }
+    
+}
diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/ContentsCache.php b/framework/ActiveSync/lib/Horde/ActiveSync/ContentsCache.php
new file mode 100644 (file)
index 0000000..863fab9
--- /dev/null
@@ -0,0 +1,16 @@
+<?php
+/**
+ * @TODO: Can't figure out what this was meant to do, and in fact the
+ * original Z-Push code that instantiates the Z-Push version of this class
+ * called methods that don't exist here.
+ *
+ *  Looks like it's just a sort of placeholder class??
+ */
+class Horde_ActiveSync_ContentsCache
+{
+    public function ImportMessageChange($message) { return true; }
+    public function ImportMessageDeletion($message) { return true; }
+    public function ImportMessageReadFlag($message) { return true; }
+    public function ImportMessageMove($message) { return true; }
+
+}
\ No newline at end of file
diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Driver/Base.php b/framework/ActiveSync/lib/Horde/ActiveSync/Driver/Base.php
new file mode 100644 (file)
index 0000000..aab1b64
--- /dev/null
@@ -0,0 +1,578 @@
+<?php
+/**
+ * Base class for ActiveSync backends. Provides the communication between
+ * the ActiveSync classes and the actual backend data that is being sync'd.
+ *
+ * Also responsible for providing objects to the command objects that can
+ * generate the delta between the PIM and server.
+ *
+ * Based, in part, on code by the Z-Push project. Original copyright notices
+ * appear below.
+ *
+ * Copyright 2010 The Horde Project (http://www.horde.org)
+ *
+ * @author Michael J. Rubinsky <mrubinsk@horde.org>
+ * @package Horde_ActiveSync
+ */
+/**
+ * File      :   diffbackend.php
+ * Project   :   Z-Push
+ * Descr     :   We do a standard differential
+ *               change detection by sorting both
+ *               lists of items by their unique id,
+ *               and then traversing both arrays
+ *               of items at once. Changes can be
+ *               detected by comparing items at
+ *               the same position in both arrays.
+ *
+ * Created   :   01.10.2007
+ *
+ * Zarafa Deutschland GmbH, www.zarafaserver.de
+ * This file is distributed under GPL v2.
+ * Consult LICENSE file for details
+ */
+abstract class Horde_ActiveSync_Driver_Base
+{
+    /**
+     * The username to sync with the backend as
+     *
+     * @var string
+     */
+    protected $_user;
+
+    /**
+     * Authenticating user
+     *
+     * @var string
+     */
+    protected $_authUser;
+
+    /**
+     * User password
+     *
+     * @var string
+     */
+    protected $_authPass;
+
+    /**
+     * Logger instance
+     *
+     * @var Horde_Log_Logger
+     */
+    protected $_logger;
+
+    /**
+     * Parameters
+     *
+     * @var array
+     */
+    protected $_params;
+
+    /**
+     * The state object for this request. Needs to be injected into this class.
+     * Different Sync objects may require more then one type of stateObject.
+     * For instance, Horde can sync contacts and caledar data with a history
+     * based state engine, but cannot due the same for email.
+     *
+     * @var Horde_ActiveSync_State_Base
+     */
+    protected $_stateObject;
+
+    /**
+     * Const'r
+     *
+     * @param array $params  Any configuration parameters or injected objects
+     *                       the concrete driver may need.
+     *  <pre>
+     *     (optional) logger       Horde_Log_Logger instance
+     *     (required) state_basic  A Horde_ActiveSync_State_Base object that is
+     *                             capable of handling all collections except
+     *                             email.
+     *     (optional) state_email  A Horde_ActiveSync_State_Base object that is
+     *                             capable of handling email collections.
+     *  </pre>
+     *
+     * @return Horde_ActiveSync_Driver
+     */
+    public function __construct($params = array())
+    {
+        $this->_params = $params;
+
+        if (empty($params['state_basic']) ||
+            !($params['state_basic'] instanceof Horde_ActiveSync_State_Base)) {
+
+            throw new Horde_ActiveSync_Exception('Missing required state object');
+        }
+
+        // Create a stub if we don't have a useable logger.
+        if (isset($params['logger'])
+            && is_callable(array($params['logger'], 'log'))) {
+            $this->_logger = $params['logger'];
+            unset($params['logger']);
+        } else {
+            $this->_logger = new Horde_Support_Stub;
+        }
+
+        $this->_stateObject = $params['state_basic'];
+        $this->_stateObject->setLogger($this->_logger);
+        $this->_stateObject->setBackend($this);
+    }
+
+    /**
+     * Setter for the logger instance
+     *
+     * @param Horde_Log_Logger $logger  The logger
+     *
+     * @void
+     */
+    public function setLogger(Horde_Log_Logger $logger)
+    {
+        $this->_logger = $logger;
+    }
+
+    /**
+     * Get folder stat
+     *  "id" => The server ID that will be used to identify the folder.
+     *          It must be unique, and not too long. How long exactly is not
+     *          known, but try keeping it under 20 chars or so.
+     *          It must be a string.
+     *  "parent" => The server ID of the parent of the folder. Same restrictions
+     *              as 'id' apply.
+     *  "mod" => This is the modification signature. It is any arbitrary string
+     *           which is constant as long as the folder has not changed. In
+     *           practice this means that 'mod' can be equal to the folder name
+     *           as this is the only thing that ever changes in folders.
+     */
+    abstract public function statFolder($id);
+
+    /**
+     * Get a folder from the backend
+     *
+     * To be implemented by concrete backend driver.
+     */
+    abstract public function getFolder($id);
+
+    /**
+     * Get the list of folders from the backend.
+     */
+    abstract public function getFolderList();
+
+    /**
+     * Get a full list of messages on the server
+     *
+     * @param string $folderId       The folder id
+     * @param timestamp $cutOffDate  The timestamp of the earliest date for
+     *                               calendar or mail entries
+     *
+     * @return array  A list of messages
+     */
+    abstract public function GetMessageList($folderId, $cutOffDate);
+
+    /**
+     * Get a list of server changes that occured during the specified time
+     * period.
+     *
+     * @param string $folderId    The server id of the collection to check.
+     * @param timestamp $from_ts  The starting timestamp
+     * @param timestamp $to_ts    The ending timestamp
+     *
+     * @return array A list of messge uids that have chnaged in the specified
+     *               time period.
+     */
+    abstract public function getServerChanges($folderId, $from_ts, $to_ts);
+
+    /**
+     * Get a message stat.
+     *
+     * @param string $folderId  The folder id
+     * @param string $id        The message id (??)
+     *
+     * @return hash with 'id', 'mod', and 'flags' members
+     */
+    abstract public function StatMessage($folderId, $id);
+
+    /**
+     *
+     * @param $folderid
+     * @param $id
+     * @param $truncsize
+     * @param $mimesupport
+     *
+     * @return Horde_ActiveSync_Message_Base The message data
+     */
+    abstract public function GetMessage($folderid, $id, $truncsize, $mimesupport = 0);
+
+    /**
+     * Delete a message
+     *
+     * @param string $folderId  Folder id
+     * @param string $id        Message id
+     *
+     * @return boolean
+     */
+    abstract public function DeleteMessage($folderid, $id);
+
+    /**
+     * Change (i.e. add or edit) a message on the backend
+     *
+     * @param string $folderId  Folderid
+     * @param string $id        Message id (maybe reorder parameteres since this may be null)
+     * @param Horde_ActiveSync_Message_Base $message
+     *
+     * @return a stat array of the new message
+     */
+    abstract public function ChangeMessage($folderid, $id, $message);
+
+    /**
+     * Any code needed to authenticate to backend as the actual user.
+     *
+     * @param string $username  The username to authenticate as
+     * @param string $password  The password
+     * @param string $domain    The user domain
+     *
+     * @return boolean
+     */
+    public function logon($username, $password, $domain = null)
+    {
+        $this->_authUser = $username;
+        $this->_authPass = $password;
+
+        return true;
+    }
+
+    /**
+     * Any code to run on log off
+     *
+     * @return boolean
+     */
+    public function Logoff()
+    {
+        return true;
+    }
+
+    /**
+     * Setup sync parameters. The user provided here is the user the backend
+     * will sync with. This allows you to authenticate as one user, and sync as
+     * another, if the backend supports this.
+     *
+     * @param string $user The username to sync as on the backend.
+     *
+     * @return boolean
+     */
+    public function setup($user)
+    {
+        $this->_user = $user;
+
+        return true;
+    }
+
+    /**
+     * Return the helper for importing hierarchy changes from the PIM.
+     *
+     * @TODO: Probably not functional, as methods were missing from original
+     * codebase.
+     *
+     * @return Horde_ActiveSync_DiffState_ImportHierarchy
+     */
+    public function GetHierarchyImporter()
+    {
+        $importer = new Horde_ActiveSync_DiffState_ImportHierarchy($this);
+        $importer->setLogger($this->_logger);
+
+        return $importer;
+    }
+
+    /**
+     * Return the helper for importing message changes from the PIM.
+     *
+     * @param string $folderid
+     *
+     * @return Horde_ActiveSync_DiffState_ImportContents
+     */
+    public function GetContentsImporter($folderId)
+    {
+        $importer = new Horde_ActiveSync_DiffState_ImportContents($this, $folderId);
+        $importer->setLogger($this->_logger);
+
+        return $importer;
+    }
+
+    /**
+     * @TODO: This will replace the above two methods
+     * @return Horde_ActiveSync_Importer
+     */
+    public function getImporter()
+    {
+        $importer = new Horde_ActiveSync_Importer($this);
+        //$importer->setLogger($this->_logger);
+        return $importer;
+    }
+
+    /**
+     * Return helper for performing the actual sync operation.
+     *
+     * @param string $folderId
+     * @return unknown_type
+     *
+     */
+    public function getExporter()
+    {
+        $exporter = new Horde_ActiveSync_Exporter($this);
+        $exporter->setLogger($this->_logger);
+
+        return $exporter;
+    }
+
+    /**
+     * Will (eventually) return an appropriate state object based on the class
+     * being sync'd.
+     * @param <type> $collection
+     */
+    public function &getStateObject($collection = array())
+    {
+        $this->_stateObject->init($collection);
+        $this->_stateObject->setLogger($this->_logger);
+        return $this->_stateObject;
+    }
+
+    /**
+     * Get the full folder hierarchy from the backend.
+     *
+     * @return array
+     */
+    public function GetHierarchy()
+    {
+        $folders = array();
+
+        $fl = $this->getFolderList();
+        foreach ($fl as $f) {
+            $folders[] = $this->getFolder($f['id']);
+        }
+
+        return $folders;
+    }
+
+    /**
+     * Obtain a message from the backend.
+     *
+     * @TODO: Not sure why we have this *and* GetMessage()??
+     *
+     * @param string $folderid
+     * @param string $id
+     * @param ?? $mimesupport  (Not sure what this was supposed to do)
+     *
+     * @return Horde_ActiveSync_Message_Base The message data
+     */
+    public function Fetch($folderid, $id, $mimesupport = 0)
+    {
+        // Forces entire message (up to 1Mb)
+        return $this->GetMessage($folderid, $id, 1024 * 1024, $mimesupport);
+    }
+
+    /**
+     *
+     * @param $attname
+     * @return unknown_type
+     */
+    public function GetAttachmentData($attname)
+    {
+        return false;
+    }
+
+    /**
+     * @param $rfc822
+     * @param $forward
+     * @param $reply
+     * @param $parent
+     * @return unknown_type
+     */
+    public function SendMail($rfc822, $forward = false, $reply = false, $parent = false)
+    {
+        return true;
+    }
+
+    /**
+     * @return unknown_type
+     */
+    public function GetWasteBasket()
+    {
+        return false;
+    }
+
+    /**
+     * @TODO: Missing method from Z-Push
+     *
+     * @param $parent
+     * @param $id
+     * @return unknown_type
+     */
+    public function DeleteFolder($parent, $id)
+    {
+        throw new Horde_ActiveSync_Exception('DeleteFolder not yet implemented');
+    }
+
+    /**
+     *
+     * @param $folderid
+     * @param $id
+     * @param $flags
+     * @return unknown_type
+     */
+    function SetReadFlag($folderid, $id, $flags)
+    {
+        return false;
+    }
+
+    /**
+     * @TODO: This method was missing from Z-Push
+     *
+     * @param unknown_type $parent
+     * @param unknown_type $id
+     * @param unknown_type $displayname
+     * @param unknown_type $type
+     * @return unknown_type
+     */
+    public function changeFolder($parent, $id, $displayname, $type)
+    {
+        throw new Horde_ActiveSync_Exception('changeFolder not yet implemented.');
+    }
+
+    /**
+     * @todo
+     *
+     * @param $folderid
+     * @param $id
+     * @param $newfolderid
+     * @return unknown_type
+     */
+    public function MoveMessage($folderid, $id, $newfolderid)
+    {
+        throw new Horde_ActiveSync_Exception('moveMessage not yet implemented.');
+    }
+
+    /**
+     * @todo
+     *
+     * @param $requestid
+     * @param $folderid
+     * @param $error
+     * @param $calendarid
+     * @return unknown_type
+     */
+    public function MeetingResponse($requestid, $folderid, $error, &$calendarid)
+    {
+        throw new Horde_ActiveSync_Exception('meetingResponse not yet implemented.');
+    }
+
+    /**
+     * Returns array of items which contain contact information
+     *
+     * @param string $searchquery
+     *
+     * @return array
+     */
+    public function getSearchResults($searchquery)
+    {
+        throw new Horde_ActiveSync_Exception('getSearchResults not implemented.');
+    }
+
+    /**
+     * Checks if the sent policykey matches the latest policykey on the server
+     * TODO: Revisit this once we have refactored state storage
+     * @param string $policykey
+     * @param string $devid
+     *
+     * @return status flag
+     */
+    public function CheckPolicy($policyKey, $devId)
+    {
+        $status = SYNC_PROVISION_STATUS_SUCCESS;
+//        $user_policykey = $this->getPolicyKey($this->_authUser, $this->_authPass, $devId);
+//        if ($user_policykey != $policyKey) {
+//            $status = SYNC_PROVISION_STATUS_POLKEYMISM;
+//        }
+//
+        return $status;
+    }
+
+    /**
+     * Return a policy key for given user with a given device id.
+     * If there is no combination user-deviceid available, a new key
+     * should be generated.
+     *
+     * @param string $user
+     * @param string $pass
+     * @param string $devid
+     *
+     * @return unknown
+     */
+    public function getPolicyKey($user, $pass, $devid)
+    {
+        return false;
+    }
+
+    /**
+     * Generate a random policy key. Right now it's a 10-digit number.
+     *
+     * @return unknown
+     */
+    public function generatePolicyKey()
+    {
+        return mt_rand(1000000000, 9999999999);
+    }
+
+    /**
+     * Set a new policy key for the given device id.
+     *
+     * @param string $policykey
+     * @param string $devid
+     * @return unknown
+     */
+    public function setPolicyKey($policykey, $devid)
+    {
+        return false;
+    }
+
+    /**
+     * Return a device wipe status
+     *
+     * @param string $user
+     * @param string $pass
+     * @param string $devid
+     * @return int
+     */
+    public function getDeviceRWStatus($devid)
+    {
+        return false;
+    }
+
+    /**
+     * Set a new rw status for the device
+     *
+     * @param string $user
+     * @param string $pass
+     * @param string $devid
+     * @param string $status
+     *
+     * @return boolean
+     */
+    public function setDeviceRWStatus($devid, $status)
+    {
+        return false;
+    }
+
+    /**
+     *
+     * @return unknown_type
+     */
+    public function AlterPing()
+    {
+        return false;
+    }
+
+    public function AlterPingChanges($folderid, &$syncstate)
+    {
+        return array();
+    }
+
+}
\ No newline at end of file
diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Driver/Horde.php b/framework/ActiveSync/lib/Horde/ActiveSync/Driver/Horde.php
new file mode 100644 (file)
index 0000000..199804a
--- /dev/null
@@ -0,0 +1,725 @@
+<?php
+/**
+ * Horde backend. Provides the communication between horde data and
+ * ActiveSync server.  Some code based on an implementation found on Z-Push's
+ * fourm. Original header appears below. All other changes are:
+ *
+ * Copyright 2010 The Horde Project (http://www.horde.org)
+ *
+ * @author Michael J. Rubinsky <mrubinsk@horde.org>
+ * @package Horde_ActiveSync
+ */
+/***********************************************
+* File      :   horde.php
+* Project   :   Z-Push
+* Descr     :   Horde backend
+* Created   :   09.03.2009
+*
+* ï¿½ Holger de Carne holger@carne.de
+* This file is distributed under GPL v2.
+* Consult LICENSE file for details
+************************************************/
+class Horde_ActiveSync_Driver_Horde extends Horde_ActiveSync_Driver_Base
+{
+
+    /** Constants **/
+    const APPOINTMENTS_FOLDER = 'Calendar';
+    const CONTACTS_FOLDER = 'Contacts';
+    const TASKS_FOLDER = 'Tasks';
+
+    /**
+     * Used for profiling
+     *
+     * @var timestamp
+     */
+    private $_starttime;
+
+    /**
+     * Cache message stats
+     *
+     * @var Array of stat hashes
+     */
+    private $_modCache;
+
+    /**
+     * Horde connector instance
+     *
+     * @var Horde_ActiveSync_Driver_Horde_Connector_Registry
+     */
+    private $_connector;
+
+    /**
+     * Const'r
+     *
+     * @param array $params  Configuration parameters.
+     *
+     * @return Horde_ActiveSync_Driver_Horde
+     */
+    public function __construct($params = array())
+    {
+        parent::__construct($params);
+        if (empty($this->_params['connector'])) {
+            throw new Horde_ActiveSync_Exception('Missing required connector object.');
+        }
+        $this->_connector = $params['connector'];
+    }
+
+    /**
+     * Authenticate to Horde
+     *
+     * @TODO: Need to inject the auth handler (waiting for rpc.php refactor)
+     *
+     * @see framework/ActiveSync/lib/Horde/ActiveSync/Driver/Horde_ActiveSync_Driver_Base#Logon($username, $domain, $password)
+     */
+    public function logon($username, $password, $domain = null)
+    {
+        $this->_logger->debug('Horde_ActiveSync_Driver_Horde::logon');
+        parent::logon($username, $password, $domain);
+
+        $this->_startProfiling();
+        $auth = Horde_Auth::singleton($GLOBALS['conf']['auth']['driver']);
+
+        return $auth->authenticate($username, array('password' => $password));
+    }
+
+    /**
+     * Clean up
+     *
+     * @see framework/ActiveSync/lib/Horde/ActiveSync/Driver/Horde_ActiveSync_Driver_Base#Logoff()
+     */
+    public function Logoff()
+    {
+        $this->_logger->debug('Horde_ActiveSync_Driver_Horde::logoff');
+        $this->_endProfiling();
+        return true;
+    }
+
+    /**
+     * Setup sync parameters. The user provided here is the user the backend
+     * will sync with. This allows you to authenticate as one user, and sync as
+     * another, if the backend supports this.
+     *
+     * @param string $user      The username to sync as on the backend.
+     *
+     * @return boolean
+     */
+    public function setup($user)
+    {
+        $this->_logger->debug('Horde::Setup(' . $user . ')');
+        parent::setup($user);
+        $this->_modCache = array();
+        return true;
+    }
+
+    /**
+     * @todo
+     *
+     * @see framework/ActiveSync/lib/Horde/ActiveSync/Driver/Horde_ActiveSync_Driver_Base#SendMail($rfc822, $forward, $reply, $parent)
+     */
+    public function SendMail($rfc822, $forward = false, $reply = false, $parent = false)
+    {
+        $this->_logger->debug('Horde::SendMail(...)');
+
+        return false;
+    }
+
+    /**
+     * @TODO
+     *
+     * @see framework/ActiveSync/lib/Horde/ActiveSync/Driver/Horde_ActiveSync_Driver_Base#GetWasteBasket()
+     */
+    public function GetWasteBasket()
+    {
+        $this->_logger->debug('Horde::GetWasteBasket()');
+
+        return false;
+    }
+
+    /**
+     * Return a list of available folders
+     *
+     * @return array  An array of folder stats
+     */
+    public function getFolderList()
+    {
+        $this->_logger->debug('Horde::getFolderList()');
+        $folders = array();
+
+        // @TODO: Be able to configure the folders to sync/not sync
+        $folders[] = $this->StatFolder(self::APPOINTMENTS_FOLDER);
+        $folders[] = $this->StatFolder(self::CONTACTS_FOLDER);
+        $folders[] = $this->StatFolder(self::TASKS_FOLDER);
+
+        return $folders;
+    }
+
+    /**
+     * Retrieve folder
+     *
+     * @param string $id  The folder id
+     *
+     * @return Horde_ActiveSync_Message_Folder
+     */
+    public function getFolder($id)
+    {
+        $this->_logger->debug('Horde::getFolder(' . $id . ')');
+
+        $folder = new Horde_ActiveSync_Message_Folder();
+        $folder->serverid = $id;
+        $folder->parentid = "0";
+        $folder->displayname = $id;
+
+        switch ($id) {
+        case self::APPOINTMENTS_FOLDER:
+            $folder->type = SYNC_FOLDER_TYPE_APPOINTMENT;
+            break;
+        case self::CONTACTS_FOLDER:
+            $folder->type = SYNC_FOLDER_TYPE_CONTACT;
+            break;
+        case self::TASKS_FOLDER:
+            $folder->type = SYNC_FOLDER_TYPE_TASK;
+            break;
+        default:
+            return false;
+        }
+
+        return $folder;
+    }
+
+    /**
+     * Stat folder
+     *
+     * @param $id
+     *
+     * @return a stat hash
+     */
+    public function StatFolder($id)
+    {
+        $this->_logger->debug('Horde::StatFolder(' . $id . ')');
+
+        $folder = array();
+        $folder['id'] = $id;
+        $folder['mod'] = $id;
+        $folder['parent'] = 0;
+
+        return $folder;
+    }
+
+    /**
+     * Get the message list of specified folder
+     *
+     * @see framework/ActiveSync/lib/Horde/ActiveSync/Driver/Horde_ActiveSync_Driver_Base#GetMessageList($folderId, $cutOffDate)
+     */
+    public function GetMessageList($folderid, $cutoffdate)
+    {
+        $this->_logger->debug('Horde::GetMessageList(' . $folderid . ', ' . $cutoffdate . ')');
+
+        $messages = array();
+        switch ($folderid) {
+        case self::APPOINTMENTS_FOLDER:
+            $startstamp = (int)$cutoffdate;
+            $endstamp = time() + 32140800; //60 * 60 * 24 * 31 * 12 == one year
+
+            try {
+                $events = $this->_connector->calendar_listEvents($startstamp, $endstamp, null);
+            } catch (Horde_Exception $e) {
+                $this->_logger->err($e->GetMessage());
+
+                return false;
+            }
+            foreach ($events as $day) {
+                foreach($day as $e) {
+                    $messages[] = $this->_smartStatMessage($folderid, $e->uid, false);
+                }
+            }
+            break;
+
+        case self::CONTACTS_FOLDER:
+            try {
+                $contacts = $this->_connector->contacts_list();
+            } catch (Horde_Exception $e) {
+                $this->_logger->err($e->GetMessage());
+
+                return false;
+            }
+
+            foreach ($contacts as $contact) {
+                $messages[] = $this->_smartStatMessage($folderid, $contact, true);
+            }
+            break;
+
+        case self::TASKS_FOLDER:
+            // @TODO?
+        default:
+            return false;
+        }
+
+        return $messages;
+    }
+
+    /**
+     * Get a list of server changes that occured during the specified time
+     * period.
+     *
+     * @param string $folderId    The server id of the collection to check.
+     * @param timestamp $from_ts  The starting timestamp
+     * @param timestamp $to_ts    The ending timestamp
+     *
+     * @return array A list of messge uids that have chnaged in the specified
+     *               time period.
+     */
+    public function getServerChanges($folderId, $from_ts, $to_ts)
+    {
+        $adds = $this->_connector->calendar_listBy('add', $from_ts);
+        $changes = $this->_connector->calendar_listBy('modify', $from_ts);
+        $deletes = $this->_connector->calendar_listBy('delete', $from_ts);
+
+        // FIXME: Need to filter the results by $from_ts OR need to fix
+        // Horde_History to query for a timerange instead of a single timestamp
+
+        return $changes;
+    }
+
+    /**
+     * Get a message from the backend
+     *
+     * @TODO: Default value for truncsize? Do we need it?
+     * @see framework/ActiveSync/lib/Horde/ActiveSync/Driver/Horde_ActiveSync_Driver_Base#GetMessage($folderid, $id, $truncsize, $mimesupport)
+     */
+    public function GetMessage($folderid, $id, $truncsize, $mimesupport = 0)
+    {
+        $this->_logger->debug('Horde::GetMessage(' . $folderid . ', ' . $id . ')');
+
+        $message = false;
+        switch ($folderid) {
+        case self::APPOINTMENTS_FOLDER:
+            try {
+                return $this->_connector->calendar_export($id);
+            } catch (Horde_Exception $e) {
+                $this->_logger->err($e->GetMessage());
+                return false;
+            }
+            //$message = self::_fromVCalendar($event);
+
+            break;
+
+        case self::CONTACTS_FOLDER:
+            try {
+                $contact = $this->_connector->contacts_export($id, 'array');
+            } catch (Horde_Exception $e) {
+                $this->_logger->err($e->GetMessage());
+                return false;
+            }
+
+            $message = self::_fromHash($contact);
+            break;
+
+        case self::TASKS_FOLDER:
+            // @TODO
+            break;
+        default:
+            return false;
+        }
+
+        return $message;
+    }
+
+    /**
+     * Get message stat data
+     *
+     * @see framework/ActiveSync/lib/Horde/ActiveSync/Driver/Horde_ActiveSync_Driver_Base#StatMessage($folderId, $id)
+     */
+    public function StatMessage($folderid, $id)
+    {
+        $this->_logger->debug('Horde::StatMessage(' . $folderid . ', ' . $id . ')');
+        return $this->_smartStatMessage($folderid, $id, true);
+    }
+
+    /**
+     * Delete a message
+     *
+     * @see framework/ActiveSync/lib/Horde/ActiveSync/Driver/Horde_ActiveSync_Driver_Base#DeleteMessage($folderid, $id)
+     */
+    public function DeleteMessage($folderid, $id)
+    {
+        $this->_logger->debug('Horde::DeleteMessage(' . $folderid . ', ' . $id . ')');
+
+        $status = false;
+        switch ($folderid) {
+        case self::APPOINTMENTS_FOLDER:
+            try {
+                $status = $this->_connector->calendar_delete($id);
+            } catch (Horde_Exception $e) {
+                $this->_logger->err($e->getMessage());
+                return false;
+            }
+            break;
+
+        case self::CONTACTS_FOLDER:
+            try {
+                $status = $this->_connector->contacts_delete($id);
+            } catch (Horde_Exception $e) {
+                $this->_logger->err($e->getMessage());
+                return false;
+            }
+            break;
+
+        case self::TASKS_FOLDER:
+            break;
+        default:
+            return false;
+        }
+
+        return $status;
+    }
+
+    /**
+     * Add/Edit a message
+     *
+     * @param string $folderid
+     * @param string $id
+     * @param Horde_ActiveSync_Message_Base $message
+     *
+     * @see framework/ActiveSync/lib/Horde/ActiveSync/Driver/Horde_ActiveSync_Driver_Base#ChangeMessage($folderid, $id, $message)
+     */
+    public function ChangeMessage($folderid, $id, $message)
+    {
+        $this->_logger->debug('Horde::ChangeMessage(' . $folderid . ', ' . $id . ')');
+
+        $stat = false;
+        switch ($folderid) {
+        case self::APPOINTMENTS_FOLDER:
+            if (!$id) {
+                try {
+                    $id = $this->_connector->calendar_import($message);
+                } catch (Horde_Exception $e) {
+                    $this->_logger->err($e->getMessage());
+                    return false;
+                }
+                $stat = $this->_smartStatMessage($folderid, $id, false);
+            } else {
+                // ActiveSync messages do NOT contain the serverUID value, put
+                // it in ourselves so we can have it during import/change.
+                $message->setServerUID($id);
+                try {
+                    $this->_connector->calendar_replace($id, $message);
+                } catch (Horde_Exception $e) {
+                    $this->_logger->err($e->getMessage());
+                    return false;
+                }
+                $stat = $this->_smartStatMessage($folderid, $id, false);
+            }
+            break;
+
+        case self::CONTACTS_FOLDER:
+            $content = self::_toHash($message);
+            if (!$id) {
+                try {
+                    $id = $this->_connector->contacts_import($content, 'array');
+                } catch (Horde_Exception $e) {
+                    $this->_logger->err($e->getMessage());
+                    return false;
+                }
+                $stat = $this->_smartStatMessage($folderid, $id, false);
+            } else {
+                try {
+                    $this->_connector->contacts_replace($id, $content, 'array');
+                } catch (Horde_Exception $e) {
+                    $this->_logger->err($e->getMessage());
+                    return false;
+                }
+                $stat = $this->_smartStatMessage($folderid, $id, false);
+            }
+        case self::TASKS_FOLDER:
+            break;
+        default:
+            return false;
+        }
+        return $stat;
+    }
+
+    /**
+     *
+     * @param string $folderid  The folder id
+     * @param string $id        The message id
+     * @param mixed $hint       ??
+     *
+     * @return message stat hash
+     */
+    private function _smartStatMessage($folderid, $id, $hint)
+    {
+        $this->_logger->debug('ActiveSync_Driver_Horde::_smartStatMessage:' . $folderid . ':' . $id);
+        $statKey = $folderid . $id;
+        $mod = false;
+        if ($hint !== false && isset($this->_modCache[$statKey])) {
+            $mod = $this->_modCache[$statKey];
+        } elseif (is_int($hint)) {
+            $mod = $hint;
+            $this->_modCache[$statKey] = $mod;
+        } else {
+            switch ($folderid) {
+            case self::APPOINTMENTS_FOLDER:
+                $mod = $this->_connector->calendar_getActionTimestamp($id, 'modify');
+                break;
+            case self::CONTACTS_FOLDER:
+                $mod = $this->_connector->contacts_getActionTimestamp($id, 'modify');
+                break;
+            case self::TASKS_FOLDER:
+                break;
+            default:
+                return false;
+            }
+            $this->_modCache[$statKey] = $mod;
+        }
+        $message = array();
+        $message['id'] = $id;
+        $message['mod'] = $mod;
+        $message['flags'] = 1;
+
+        return $message;
+    }
+
+    /**
+     * Start profiling data
+     *
+     * @return void
+     */
+    private function _startProfiling()
+    {
+        $this->_starttime = microtime(true);
+    }
+
+    /**
+     * End profiling
+     *
+     * @return void
+     */
+    private function _endProfiling()
+    {
+        $this->_logger->debug('Session lasted ' . sprintf('%0.3f',microtime(true) - $this->_starttime) . ' s');
+    }
+
+
+    /**
+     * Create a hash suitable for importing into contacts/import or
+     * contacts/replace from a Horde_ActiveSync_Message_Base object.
+     *
+     * @param Horde_ActiveSync_Message_Contact $message
+     *
+     * @return array
+     */
+    private static function _toHash(Horde_ActiveSync_Message_Contact $message)
+    {
+        $charset = Horde_Nls::getCharset();
+        $formattedname = false;
+
+        /* Name */
+        $hash['name'] = Horde_String::convertCharset($message->fileas, 'utf-8', $charset);
+        $hash['lastname'] = Horde_String::convertCharset($message->lastname, 'utf-8', $charset);
+        $hash['firstname'] = Horde_String::convertCharset($message->firstname, 'utf-8', $charset);
+        $hash['middlenames'] = Horde_String::convertCharset($message->middlename, 'utf-8', $charset);
+        $hash['namePrefix'] = Horde_String::convertCharset($message->title, 'utf-8', $charset);
+        $hash['nameSuffix'] = Horde_String::convertCharset($message->suffix, 'utf-8', $charset);
+
+
+        // picture ($message->picture *should* already be base64 encdoed)
+        $hash['photo'] = base64_decode($message->picture);
+
+        /* Home */
+        $hash['homeStreet'] = Horde_String::convertCharset($message->homestreet, 'utf-8', $charset);
+        $hash['homeCity'] = Horde_String::convertCharset($message->homecity, 'utf-8', $charset);
+        $hash['homeProvince'] = Horde_String::convertCharset($message->homestate, 'utf-8', $charset);
+        $hash['homePostalCode'] = $message->homepostalcode;
+        $hash['homeCountry'] = Horde_String::convertCharset($message->homecountry, 'utf-8', $charset);
+
+        /* Business */
+        $hash['workStreet'] = Horde_String::convertCharset($message->businessstreet, 'utf-8', $charset);
+        $hash['workCity'] = Horde_String::convertCharset($message->businesscity, 'utf-8', $charset);
+        $hash['workProvince'] = Horde_String::convertCharset($message->businessstate, 'utf-8', $charset);
+        $hash['workPostalCode'] = $message->businesspostalcode;
+        $hash['workCountry'] = Horde_String::convertCharset($message->businesscountry, 'utf-8', $charset);
+
+        $hash['homePhone'] = $message->homephonenumber;
+        $hash['workPhone'] = $message->businessphonenumber;
+        $hash['fax'] = $message->businessfaxnumber;
+        $hash['pager'] = $message->pagernumber;
+        $hash['cellPhone'] = $message->mobilephonenumber;
+
+        /* Email addresses */
+        $hash['email'] = Horde_iCalendar_vcard::getBareEmail($message->email1address);
+
+        /* Job title */
+        $hash['title'] = Horde_String::convertCharset($message->jobtitle, 'utf-8', $charset);
+
+        $hash['company'] = Horde_String::convertCharset($message->companyname, 'utf-8', $charset);
+        $hash['department'] = Horde_String::convertCharset($message->department, 'utf-8', $charset);
+
+        /* Categories */
+        $hash['category']['value'] = Horde_String::convertCharset(implode(';', $message->categories), 'utf-8', $charset);
+        $hash['category']['new'] = true;
+        /* Children */
+        // @TODO
+
+        /* Spouse */
+        $hash['spouse'] = Horde_String::convertCharset($message->spouse, 'utf-8', $charset);
+
+        /* Notes */
+        $hash['notes'] = Horde_String::convertCharset($message->body, 'utf-8', $charset);
+
+        /* webpage */
+        $hash['website'] = Horde_String::convertCharset($message->webpage, 'utf-8', $charset);
+
+        /* Birthday and Anniversary */
+        if (!empty($message->birthday)) {
+            $bday = new Horde_Date($message->birthday);
+            $hash['birthday'] = $bday->format('Y-m-d');
+        } else {
+            $hash['birthday'] = null;
+        }
+        if (!empty($message->anniversary)) {
+            $anniversary = new Horde_Date($message->anniversary);
+            $hash['anniversary'] = $anniversary->format('Y-m-d');
+        } else {
+            $hash['anniversary'] = null;
+        }
+
+        /* Assistant */
+        $hash['assistant'] = Horde_String::convertCharset($message->assistantname, 'utf-8', $charset);
+
+        return $hash;
+    }
+
+    /**
+     * Import data from Horde's contacts API
+     *
+     * @param array $hash  A hash as returned from contacts/export
+     *
+     * @return Horde_ActiveSync_Message_Base object
+     */
+    private static function _fromHash($hash)
+    {
+        $message = new Horde_ActiveSync_Message_Contact();
+
+        $charset = Horde_Nls::getCharset();
+
+        foreach ($hash as $field => $value) {
+            switch ($field) {
+            case 'name':
+                $message->fileas = Horde_String::convertCharset($value, $charset, 'utf-8');
+                break;
+            case 'lastname':
+                $message->lastname = Horde_String::convertCharset($value, $charset, 'utf-8');
+                break;
+            case 'firstname':
+                $message->firstname = Horde_String::convertCharset($value, $charset, 'utf-8');
+                break;
+            case 'middlenames':
+                $message->middlename = Horde_String::convertCharset($value, $charset, 'utf-8');
+                break;
+            case 'namePrefix':
+                $message->title = Horde_String::convertCharset($value, $charset, 'utf-8');
+                break;
+            case 'nameSuffix':
+                $message->suffix = Horde_String::convertCharset($value, $charset, 'utf-8');
+                break;
+
+            case 'photo':
+                $message->picture = base64_encode($value['load']['data']);
+                break;
+
+            /* Address (TODO: check for a single home/workAddress field instead) */
+            case 'homeStreet':
+                $message->homestreet = Horde_String::convertCharset($hash['homeStreet'], $charset, 'utf-8');
+                break;
+            case 'homeCity':
+                $message->homecity = Horde_String::convertCharset($hash['homeCity'], $charset, 'utf-8');
+                break;
+            case 'homeProvince':
+                $message->homestate = Horde_String::convertCharset($hash['homeProvince'], $charset, 'utf-8');
+                break;
+            case 'homePostalCode':
+                $message->homepostalcode = Horde_String::convertCharset($hash['homePostalCode'], $charset, 'utf-8');
+                break;
+            case 'homeCountry':
+                $message->homecountry = Horde_String::convertCharset($hash['homeCountry'], $charset, 'utf-8');
+                break;
+            case 'workStreet':
+                $message->businessstreet = Horde_String::convertCharset($hash['workStreet'], $charset, 'utf-8');
+                break;
+            case 'workCity':
+                $message->businesscity = Horde_String::convertCharset($hash['workCity'], $charset, 'utf-8');
+                break;
+            case 'workProvince':
+                $message->businessstate = Horde_String::convertCharset($hash['workProvince'], $charset, 'utf-8');
+                break;
+            case 'workPostalCode':
+                $message->businesspostalcode = Horde_String::convertCharset($hash['workPostalCode'], $charset, 'utf-8');
+                break;
+            case 'workCountry':
+                $message->businesscountry = Horde_String::convertCharset($hash['workCountry'], $charset, 'utf-8');
+                break;
+            case 'homePhone':
+                /* Phone */
+                $message->homephonenumber = $hash['homePhone'];
+                break;
+            case 'cellPhone':
+                $message->mobilephonenumber = $hash['cellPhone'];
+                break;
+            case 'fax':
+                $message->businessfaxnumber = $hash['fax'];
+                break;
+            case 'workPhone':
+                $message->businessphonenumber = $hash['workPhone'];
+                break;
+            case 'pager':
+                $message->pagernumber = $hash['pager'];
+                break;
+
+            case 'email':
+                $message->email1address = Horde_iCalendar_vcard::getBareEmail($value);
+                break;
+
+            case 'title':
+                $message->jobtitle = Horde_String::convertCharset($value, $charset, 'utf-8');
+                break;
+
+            case 'company':
+                $message->companyname = Horde_String::convertCharset($value, $charset, 'utf-8');
+                break;
+            case 'departnemt':
+                $message->department = Horde_String::convertCharset($value, $charset, 'utf-8');
+                break;
+
+            case 'category':
+                // Categories FROM horde are a simple string value, going BACK to horde are an array with 'value' and 'new' keys
+                $message->categories = explode(';', Horde_String::convertCharset($value, $charset, 'utf-8'));
+                break;
+
+            case 'spouse':
+                $message->spouse = Horde_String::convertCharset($value, $charset, 'utf-8');
+                break;
+            case 'notes':
+                $message->body = Horde_String::convertCharset($value, $charset, 'utf-8');
+                $message->bodysize = strlen($message->body);
+                $message->bodytruncated = false;
+                break;
+            case 'website':
+                $message->webpage = $value;
+                break;
+
+            case 'birthday':
+            case 'anniversary':
+                if (!empty($value)) {
+                    $date = new Horde_Date($value);
+                    $message->{$field} = $date;
+                } else {
+                    $message->$field = null;
+                }
+                break;
+            }
+        }
+
+        return $message;
+    }
+
+}
diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Driver/Horde/Connector/Registry.php b/framework/ActiveSync/lib/Horde/ActiveSync/Driver/Horde/Connector/Registry.php
new file mode 100644 (file)
index 0000000..86edbaa
--- /dev/null
@@ -0,0 +1,217 @@
+<?php
+/*
+ * To change this template, choose Tools | Templates
+ * and open the template in the editor.
+ */
+class Horde_ActiveSync_Driver_Horde_Connector_Registry
+{
+    /**
+     * @var Horde_Registry
+     */
+    private $_registry;
+
+    /**
+     * Const'r
+     *
+     */
+    public function __construct($params = array())
+    {
+        if (empty($params['registry'])) {
+            throw new Horde_ActiveSync_Exception('Missing required Horde_Registry object.');
+        }
+
+        $this->_registry = $params['registry'];
+    }
+
+    /**
+     * Get a list of events from horde's calendar api
+     *
+     * @param timestamp $startstamp    The start of time period.
+     * @param timestamp $endstamp      The end of time period
+     * @param string $calendar         The calendar(s) to get events for
+     *
+     * @return array
+     */
+    public function calendar_listEvents($startstamp, $endstamp, $calendar)
+    {
+        $result = $this->_registry->calendar->listEvents(
+                $startstamp,   // Start
+                $endstamp,     // End
+                $calendar,     // Calendar
+                false,         // Recurrence
+                false,         // Alarms only
+                false,         // Show remote
+                true);         // Hide exception events
+
+        return $result;
+    }
+
+    /**
+     * Get a list of event uids that have had $action happen since $from_ts.
+     * Optionally limits to a specific calendar.
+     *
+     * @param string $action      The action to check for (add, modify, delete)
+     * @param timestamp $from_ts  The timestamp to start checking from
+     * @param string $calendar
+     */
+    public function calendar_listBy($action, $from_ts, $calendar = null)
+    {
+        return $this->_registry->calendar->listBy($action, $from_ts, $calendar);
+    }
+
+    /**
+     * Export the specified calendar in the specified content type
+     *
+     * @param string $uid          The calendar id
+     * @param string $contentType  The content type specifier
+     *
+     * @return The iCalendar representation of the calendar
+     */
+    public function calendar_export($uid)
+    {
+        $result = $this->_registry->calendar->export($uid, 'activesync');
+        return $result;
+    }
+
+    /**
+     * Import an event into Horde's calendar store.
+     *
+     * @param Horde_ActiveSync_Message_Appointmetn $content  The event content
+     * @param string $contentType                            The content type of $content
+     * @param string $calendar                               The calendar to import event into
+     *
+     * @return string  The event's UID
+     */
+    public function calendar_import($content, $calendar = null)
+    {
+        return $this->_registry->calendar->import($content, 'activesync', $calendar);
+    }
+
+    /**
+     * Replcae the event with new data
+     *
+     * @param string $uid          The UID of the event to replace
+     * @param string $content      The new event content
+     * @param string $contentType  The content type of $content
+     *
+     * @return boolean
+     */
+    public function calendar_replace($uid, $content)
+    {
+        $result = $this->_registry->calendar->replace($uid, $content, 'activesync');
+        return $result;
+    }
+
+    /**
+     * Delete an event from Horde's calendar storage
+     *
+     * @param string $uid  The UID of the event to delete
+     *
+     * @return boolean
+     */
+    public function calendar_delete($uid)
+    {
+        $result = $this->_registry->calendar->delete($uid);
+        return $result;
+    }
+
+    /**
+     * Return the timestamp for the last time $action was performed.
+     *
+     * @param string $uid     The UID of the event we are interested in.
+     * @param string $action  The action we are interested in (add, modify...)
+     *
+     * @return timestamp
+     */
+    public function calendar_getActionTimestamp($uid, $action)
+    {
+        $result = $this->_registry->calendar->getActionTimestamp($uid, $action);
+        return $result;
+    }
+
+    /**
+     * Get a list of all contacts a user can see
+     *
+     * @return array of contact UIDs
+     */
+    public function contacts_list()
+    {
+        $result = $this->_registry->contacts->listContacts();
+        return $result;
+    }
+
+    /**
+     * Export the specified contact from Horde's contacts storage
+     *
+     * @param string $uid          The contact's UID
+     * @param string $contentType  The content type to export in
+     *                             (text/directory text/vcard text/x-vcard)
+     *
+     * @return the contact in the requested content type
+     */
+    public function contacts_export($uid, $contentType)
+    {
+        $result = $this->_registry->contacts->export($uid, $contentType);
+        return $result;
+    }
+
+    /**
+     * Import the provided contact data into Horde's contacts storage
+     *
+     * @param string $content      The contact data
+     * @param string $contentType  The content type specifier of $content
+     * @param string $source       The contact source to import to
+     *
+     * @return boolean
+     */
+    public function contacts_import($content, $contentType, $import_source = null)
+    {
+        $result = $this->_registry->contacts->import($content, $contentType, $import_source);
+        return $result;
+    }
+
+    /**
+     * Replace the specified contact with the data provided.
+     *
+     * @param string $uid          The UID of the contact to replace
+     * @param string $content      The contact data
+     * @param string $contentType  The content type of $content
+     * @param string $sources      The sources where UID will be replaced
+     *
+     * @return boolean
+     */
+    public function contacts_replace($uid, $content, $contentType, $sources = null)
+    {
+        $result = $this->_registry->contacts->replace($uid, $content, $contentType, $sources);
+        return $result;
+    }
+
+    /**
+     * Delete the specified contact
+     *
+     * @param string $uid  The UID of the contact to remove
+     *
+     * @return bolean
+     */
+    public function contacts_delete($uid)
+    {
+        $result = $this->_registry->contacts->delete($uid);
+        return $result;
+    }
+
+    /**
+     * Get the timestamp of the most recent occurance of $action for the
+     * specifed contact
+     *
+     * @param string $uid     The UID of the contact to search
+     * @param string $action  The action to lookup
+     *
+     * @return timestamp
+     */
+    public function contacts_getActionTimestamp($uid, $action)
+    {
+        $result = $this->_registry->contacts->getActionTimestamp($uid, $action);
+        return $result;
+    }
+
+}
\ No newline at end of file
diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Exception.php b/framework/ActiveSync/lib/Horde/ActiveSync/Exception.php
new file mode 100644 (file)
index 0000000..e380d73
--- /dev/null
@@ -0,0 +1,3 @@
+<?php
+class Horde_ActiveSync_Exception extends Exception {
+}
\ No newline at end of file
diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Exporter.php b/framework/ActiveSync/lib/Horde/ActiveSync/Exporter.php
new file mode 100644 (file)
index 0000000..cedd6f7
--- /dev/null
@@ -0,0 +1,268 @@
+<?php
+define('BACKEND_DISCARD_DATA', 1);
+
+/**
+ * Horde_ActiveSync_DiffState classes provide a basic diff engine for comparing
+ * PIM and backend state. This is a general diff engine, and can be used as-is
+ * or subclassed/overridden by individual backend drivers if they backend can
+ * provide the differential information more effeciently.
+ *
+ * Diff algorithms ported from Z-Push's diffbackend.php DiffState class, all
+ * other code and modifications:
+ *
+ * Copyright 2010 The Horde Project (http://www/horde.org)
+ *
+ * @author Michael J. Rubinsky <mrubinsk@horde.org>
+ * @package Horde_ActiveSync
+ *
+ */
+/***********************************************
+* File      :   diffbackend.php
+* Project   :   Z-Push
+* Descr     :   We do a standard differential
+*               change detection by sorting both
+*               lists of items by their unique id,
+*               and then traversing both arrays
+*               of items at once. Changes can be
+*               detected by comparing items at
+*               the same position in both arrays.
+*
+* Created   :   01.10.2007
+*
+* ï¿½ Zarafa Deutschland GmbH, www.zarafaserver.de
+* This file is distributed under GPL v2.
+* Consult LICENSE file for details
+************************************************/
+
+/**
+ * This class handles preparing the diff data for sending back to the PIM. Takes
+ * the data from the Importer, syncronizes it and tracks the state.
+ *
+ * @author Michael J. Rubinsky <mrubinsk@horde.org>
+ * @package Horde_ActiveSync
+ */
+class Horde_ActiveSync_Exporter
+{
+    /**
+     * Local copy of changes to push to PIM
+     *
+     * @var array
+     */
+    protected $_changes;
+
+    /**
+     * Tracks the number of changes that have been sent.
+     *
+     * @var int
+     */
+    protected $_step = 0;
+
+    /**
+     * Server specific folder id
+     *
+     * @var string
+     */
+    protected $_folderId;
+
+    /**
+     * The collection type for this folder
+     *
+     * @var string
+     */
+    protected $_collection;
+
+    /**
+     * The backend driver
+     *
+     * @var Horde_ActiveSync_Driver_Base
+     */
+    protected $_backend;
+
+    /**
+     * Any flags
+     * ???
+     * @var <type>
+     */
+    protected $_flags;
+
+    /**
+     * The statemachine
+     *
+     * @var Horde_ActiveSynce_StateMachine_Base
+     */
+    protected $_state;
+
+    /**
+     * The current syncKey for this request
+     *
+     * @var string
+     */
+    protected $_syncKey;
+
+    /**
+     * The change streamer
+     *
+     * @var Horde_ActiveSync_Streamer
+     */
+    protected $_streamer;
+
+    protected $_logger;
+
+    /**
+     * Const'r
+     *
+     * @param <type> $backend
+     */
+    public function __construct(Horde_ActiveSync_Driver_Base $backend)
+    {
+        $this->_backend = $backend;
+    }
+
+    public function init(Horde_ActiveSync_State_Base &$stateMachine,
+                         $streamer,
+                         $collection = array())
+    {
+        $this->_stateMachine = &$stateMachine;
+        $this->_streamer = $streamer;
+        $this->_folderId = !empty($collection['id']) ? $collection['id'] : false;
+        $this->_changes = $stateMachine->getChanges();
+        $this->_syncKey = $collection['synckey'];
+        $this->_truncation = !empty($collection['truncation']) ? $collection['truncation'] : 0;
+    }
+
+    public function setLogger($logger)
+    {
+        $this->_logger = $logger;
+    }
+
+    /**
+     * Sends the next change in the set and updates the stateMachine if
+     * successful
+     *
+     * @return mixed  A progress array or false if no more changes
+     */
+    public function syncronize($flags = 0)
+    {
+        $progress = array();
+
+        if ($this->_folderId == false) {
+            //@TODO: Folder changes not implemented??
+            if ($this->_step < count($this->_changes)) {
+                $change = $this->_changes[$this->_step];
+
+                switch($change['type']) {
+                case 'change':
+                    $folder = $this->_backend->getFolder($change['id']);
+                    $stat = $this->_backend->StatFolder($change['id']);
+                    if (!$folder) {
+                        return;
+                    }
+
+                    if ($flags & BACKEND_DISCARD_DATA || $this->_streamer->FolderChange($folder)) {
+                        $this->_stateMachine->updateState('change', $stat);
+                    }
+                    break;
+                case 'delete':
+                    if ($flags & BACKEND_DISCARD_DATA || $this->_streamer->FolderDeletion($change['id'])) {
+                        $this->_stateMachine->updateState('delete', $change);
+                    }
+                    break;
+                }
+
+                $this->_step++;
+
+                $progress = array();
+                $progress['steps'] = count($this->_changes);
+                $progress['progress'] = $this->_step;
+
+                return $progress;
+            } else {
+                return false;
+            }
+        } else {
+            if ($this->_step < count($this->_changes)) {
+                $change = $this->_changes[$this->_step];
+
+                switch($change['type']) {
+                case 'change':
+                    $truncsize = self::_getTruncSize($this->_truncation);
+                    // Note: because 'parseMessage' and 'statMessage' are two seperate
+                    // calls, we have a chance that the message has changed between both
+                    // calls. This may cause our algorithm to 'double see' changes.
+                    $stat = $this->_backend->StatMessage($this->_folderId, $change['id']);
+                    if (!$message = $this->_backend->GetMessage($this->_folderId, $change['id'], $truncsize)) {
+                        return false;
+                    }
+
+                    // copy the flag to the message
+                    $message->flags = (isset($change['flags'])) ? $change['flags'] : 0;
+
+                    if ($stat && $message) {
+                        if ($flags & BACKEND_DISCARD_DATA || $this->_streamer->messageChange($change['id'], $message) == true) {
+                            $this->_stateMachine->updateState('change', $stat);
+                        }
+                    }
+                    break;
+
+                case 'delete':
+                    if ($flags & BACKEND_DISCARD_DATA || $this->_streamer->messageDeletion($change['id']) == true) {
+                        $this->_stateMachine->updateState('delete', $change);
+                    }
+                    break;
+
+                case 'flags':
+                    if ($flags & BACKEND_DISCARD_DATA || $this->_streamer->messageReadFlag($change['id'], $change['flags']) == true) {
+                        $this->_stateMachine->updateState('flags', $change);
+                    }
+                    break;
+
+                case 'move':
+                    if ($flags & BACKEND_DISCARD_DATA || $this->_streamer->messageMove($change['id'], $change['parent']) == true) {
+                        $this->_stateMachine->updateState('move', $change);
+                    }
+                    break;
+                }
+
+                $this->_step++;
+
+                $progress = array();
+                $progress['steps'] = count($this->_changes);
+                $progress['progress'] = $this->_step;
+
+                return $progress;
+            } else {
+                return false;
+            }
+        }
+    }
+
+    public function getChangeCount()
+    {
+        return $this->_stateMachine->getChangeCount();
+    }
+
+    /**
+     *
+     * @param $truncation
+     * @return unknown_type
+     */
+    private static function _getTruncSize($truncation)
+    {
+        switch($truncation) {
+        case SYNC_TRUNCATION_HEADERS:
+            return 0;
+        case SYNC_TRUNCATION_512B:
+            return 512;
+        case SYNC_TRUNCATION_1K:
+            return 1024;
+        case SYNC_TRUNCATION_5K:
+            return 5 * 1024;
+        case SYNC_TRUNCATION_SEVEN:
+        case SYNC_TRUNCATION_ALL:
+            return 1024 * 1024; // We'll limit to 1MB anyway
+        default:
+            return 1024; // Default to 1Kb
+        }
+    }
+
+}
diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/HierarchyCache.php b/framework/ActiveSync/lib/Horde/ActiveSync/HierarchyCache.php
new file mode 100644 (file)
index 0000000..123b543
--- /dev/null
@@ -0,0 +1,47 @@
+<?php
+/**
+ * This simply collects all changes so that they can be retrieved later, for
+ * statistics gathering for example
+ */
+/**
+ * File      :   memimporter.php
+ * Project   :   Z-Push
+ * Descr     :   Classes that collect changes
+ *
+ * Created   :   01.10.2007
+ *
+ * Â© Zarafa Deutschland GmbH, www.zarafaserver.de
+ * This file is distributed under GPL v2.
+ * Consult LICENSE file for details
+ */
+class Horde_ActiveSync_HierarchyCache
+{
+    public $changed;
+    public $deleted;
+    public $count;
+
+    public function __construct()
+    {
+        $this->changed = array();
+        $this->deleted = array();
+        $this->count = 0;
+
+        return true;
+    }
+
+    public function FolderChange($folder)
+    {
+        array_push($this->changed, $folder);
+        $this->count++;
+
+        return true;
+    }
+
+    public function FolderDeletion($id)
+    {
+        array_push($this->deleted, $id);
+        $this->count++;
+
+        return true;
+    }
+}
\ No newline at end of file
diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Importer.php b/framework/ActiveSync/lib/Horde/ActiveSync/Importer.php
new file mode 100644 (file)
index 0000000..913c2ce
--- /dev/null
@@ -0,0 +1,261 @@
+<?php
+/*
+ * To change this template, choose Tools | Templates
+ * and open the template in the editor.
+ */
+class Horde_ActiveSync_Importer
+{
+    /**
+     *
+     * @var Horde_ActiveSync_StateMachine_Base
+     */
+    protected $_stateMachine;
+
+    /**
+     *
+     * @var Horde_ActiveSync_Driver_Base
+     */
+    protected $_backend;
+
+    /**
+     * Sync key for current request
+     *
+     * @var string
+     */
+    protected $_syncKey;
+
+    /**
+     * @TODO
+     * @var <type>
+     */
+    protected $_flags;
+
+    /**
+     * The server specific folder id
+     *
+     * @var string
+     */
+    protected $_folderId;
+
+    protected $_logger;
+
+    /**
+     * Const'r
+     *
+     * @param Horde_ActiveSync_Driver_Base $backend
+     * @param Horde_ActiveSync_StateMachine_Base $stateMachine
+     * @param <type> $syncKey
+     * @param <type> $flags
+     */
+    public function __construct(Horde_ActiveSync_Driver_Base $backend)
+    {
+        $this->_backend = $backend;
+    }
+
+    public function init(Horde_ActiveSync_State_Base &$stateMachine,
+                         $folderId, $syncKey, $flags = 0)
+    {
+        $this->_stateMachine = &$stateMachine;
+        $this->_syncKey = $syncKey;
+        $this->_flags = $flags;
+        $this->_folderId = $folderId;
+    }
+
+    public function setLogger($logger)
+    {
+        $this->_logger = $logger;
+    }
+
+    /**
+     *
+     * @param mixed $id                                A server message id or
+     *                                                 false if a new message
+     * @param Horde_ActiveSync_Message_Base $message   A message object
+     *
+     * @return mixed The server message id or false
+     */
+    public function ImportMessageChange($id, $message)
+    {
+        //do nothing if it is in a dummy folder
+        if ($this->_folderId == SYNC_FOLDER_TYPE_DUMMY) {
+            return false;
+        }
+
+        if ($id) {
+            // See if there's a conflict
+            $conflict = $this->_isConflict('change', $this->_folderId, $id);
+
+            // Update client state if this is an update
+            $change = array();
+            $change['id'] = $id;
+            $change['mod'] = 0; // dummy, will be updated later if the change succeeds
+            $change['parent'] = $this->_folderId;
+            $change['flags'] = (isset($message->read)) ? $message->read : 0;
+            $this->_stateMachine->updateState('change', $change);
+
+            if ($conflict && $this->_flags == SYNC_CONFLICT_OVERWRITE_PIM) {
+                return true;
+            }
+        }
+
+        $stat = $this->_backend->ChangeMessage($this->_folderId, $id, $message);
+        // @TODO: Isn't this an error?
+        if (!is_array($stat)) {
+            return $stat;
+        }
+
+        // Record the state of the message
+        $this->_stateMachine->updateState('change', $stat);
+
+        return $stat['id'];
+    }
+
+    /**
+     * Import a deletion. This may conflict if the local object has been
+     * modified.
+     *
+     * @param string $id  Server message id
+     */
+    public function ImportMessageDeletion($id)
+    {
+        //do nothing if it is in a dummy folder
+        if ($this->_folderId == SYNC_FOLDER_TYPE_DUMMY) {
+            return true;
+        }
+
+        // See if there's a conflict
+        $conflict = $this->_isConflict('delete', $this->_folderId, $id);
+
+        // Update client state
+        $change = array();
+        $change['id'] = $id;
+        $this->_stateMachine->updateState('delete', $change);
+
+        // If there is a conflict, and the server 'wins', then return OK without
+        // performing the change this will cause the exporter to 'see' the
+        // overriding item as a change, and send it back to the PIM
+        if ($conflict && $this->_flags == SYNC_CONFLICT_OVERWRITE_PIM) {
+            return true;
+        }
+
+        $this->_backend->DeleteMessage($this->_folderId, $id);
+
+        return true;
+    }
+
+    /**
+     * Import a change in 'read' flags .. This can never conflict
+     *
+     * @param string $id  Server message id
+     * @param ??  $flags  The read flags to set
+     */
+    public function ImportMessageReadFlag($id, $flags)
+    {
+        //do nothing if it is a dummy folder
+        if ($this->_folderId == SYNC_FOLDER_TYPE_DUMMY) {
+            return true;
+        }
+
+        // Update client state
+        $change = array();
+        $change['id'] = $id;
+        $change['flags'] = $flags;
+        $this->_stateMachine->updateState('flags', $change);
+        $this->_backend->SetReadFlag($this->_folderId, $id, $flags);
+
+        return true;
+    }
+
+    /**
+     * Not supported/todo?
+     *
+     * @param <type> $id
+     * @param <type> $newfolder
+     * @return <type>
+     */
+    public function ImportMessageMove($id, $newfolder)
+    {
+        return true;
+    }
+
+    /**
+     *
+     * @param $id
+     * @param $parent
+     * @param $displayname
+     * @param $type
+     * @return unknown_type
+     */
+    public function ImportFolderChange($id, $parent, $displayname, $type)
+    {
+        //do nothing if it is a dummy folder
+        if ($parent == SYNC_FOLDER_TYPE_DUMMY) {
+            return false;
+        }
+
+        if ($id) {
+            $change = array();
+            $change['id'] = $id;
+            $change['mod'] = $displayname;
+            $change['parent'] = $parent;
+            $change['flags'] = 0;
+            $this->_stateMachine->updateState('change', $change);
+        }
+
+        // @TODO: ChangeFolder did not exist in ZPush's code??
+        $stat = $this->_backend->ChangeFolder($parent, $id, $displayname, $type);
+        if ($stat) {
+            $this->_stateMachine->updateState('change', $stat);
+        }
+
+        return $stat['id'];
+    }
+
+    /**
+     *
+     * @param $id
+     * @param $parent
+     * @return unknown_type
+     */
+    public function ImportFolderDeletion($id, $parent)
+    {
+        //do nothing if it is a dummy folder
+        if ($parent == SYNC_FOLDER_TYPE_DUMMY) {
+            return false;
+        }
+
+        $change = array();
+        $change['id'] = $id;
+
+        $this->_stateMachine->updateState('delete', $change);
+        $this->_backend->DeleteFolder($parent, $id);
+
+        return true;
+    }
+
+    /**
+     *  Returns TRUE if the given ID conflicts with the given operation.
+     *  This is only true in the following situations:
+     *
+     *    Changed here and changed there
+     *    Changed here and deleted there
+     *    Deleted here and changed there
+     *
+     * Any other combination of operations can be done
+     * (e.g. change flags & move or move & delete)
+     */
+    protected function _isConflict($type, $folderid, $id)
+    {
+        $stat = $this->_backend->StatMessage($folderid, $id);
+        if (!$stat) {
+            // Message is gone
+            if ($type == 'change') {
+                return true;
+            } else {
+                return false; // all other remote changes still result in a delete (no conflict)
+            }
+        }
+
+        return $this->_stateMachine->isConflict($stat, $type);
+    }
+}
\ No newline at end of file
diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Message/Appointment.php b/framework/ActiveSync/lib/Horde/ActiveSync/Message/Appointment.php
new file mode 100644 (file)
index 0000000..6b7ed5a
--- /dev/null
@@ -0,0 +1,722 @@
+<?php
+/**
+ * Horde_ActiveSync_Message_Appointment class represents a single ActiveSync
+ * Appointment object. Responsible for mapping all fields to and from wbxml.
+ *
+ * @copyright 2010 The Horde Project (http://www.horde.org)
+ *
+ * @author Michael J. Rubinsky <mrubinsk@horde.org>
+ * @package Horde_ActiveSync
+ */
+class Horde_ActiveSync_Message_Appointment extends Horde_ActiveSync_Message_Base
+{
+    /* Sensitivity */
+    const SENSITIVITY_NORMAL = 0;
+    const SENSITIVITY_PERSONAL = 1;
+    const SENSITIVITY_PRIVATE = 2;
+    const SENSITIVITY_CONFIDENTIAL = 3;
+
+    /* Busy status */
+    const BUSYSTATUS_FREE = 0;
+    const BUSYSTATUS_TENATIVE = 1;
+    const BUSYSTATUS_BUSY = 2;
+    const BUSYSTATUS_OUT = 3;
+
+    /* All day meeting */
+    const IS_ALL_DAY = 1;
+
+    /* Meeting status */
+    const MEETING_NOT_MEETING = 0;
+    const MEETING_IS_MEETING = 1;
+    const MEETING_RECEIVED = 3;
+    const MEETING_CANCELLED = 5;
+    const MEETING_CANCELLED_RECEIVED = 7;
+
+    /* Response status */
+    const RESPONSE_NONE = 0;
+    const RESPONSE_ORGANIZER = 1;
+    const RESPONSE_TENATIVE = 2;
+    const RESPONSE_ACCEPTED =3;
+    const RESPONSE_DECLINED = 4;
+    const RESPONSE_NORESPONSE = 5; // Not sure what difference this is to NONE?
+
+    /**
+     * Workarounds for PHP < 5.2.6 not being able to return an array by reference
+     * from a __get() property.
+     */
+    public $exceptions = array();
+    public $attendees;
+    public $categories;
+
+    /**
+     * Constructor
+     *
+     * @param array $params
+     *
+     * @return Horde_ActiveSync_Message_Appointment
+     */
+    public function __construct($params = array()) {
+        $mapping = array(
+            SYNC_POOMCAL_TIMEZONE => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'timezone'),
+            SYNC_POOMCAL_DTSTAMP => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'dtstamp', Horde_ActiveSync_Message_Base::KEY_TYPE => Horde_ActiveSync_Message_Base::TYPE_DATE),
+            SYNC_POOMCAL_STARTTIME => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'starttime', Horde_ActiveSync_Message_Base::KEY_TYPE => Horde_ActiveSync_Message_Base::TYPE_DATE),
+            SYNC_POOMCAL_SUBJECT => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'subject'),
+            SYNC_POOMCAL_UID => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'uid', Horde_ActiveSync_Message_Base::KEY_TYPE => Horde_ActiveSync_Message_Base::TYPE_HEX),
+            SYNC_POOMCAL_ORGANIZERNAME => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'organizername'),
+            SYNC_POOMCAL_ORGANIZEREMAIL => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'organizeremail'),
+            SYNC_POOMCAL_LOCATION => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'location'),
+            SYNC_POOMCAL_ENDTIME => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'endtime', Horde_ActiveSync_Message_Base::KEY_TYPE => Horde_ActiveSync_Message_Base::TYPE_DATE),
+            SYNC_POOMCAL_RECURRENCE => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'recurrence', Horde_ActiveSync_Message_Base::KEY_TYPE => 'Horde_ActiveSync_Message_Recurrence'),
+            SYNC_POOMCAL_SENSITIVITY => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'sensitivity'),
+            SYNC_POOMCAL_BUSYSTATUS => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'busystatus'),
+            SYNC_POOMCAL_ALLDAYEVENT => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'alldayevent'),
+            SYNC_POOMCAL_REMINDER => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'reminder'),
+            SYNC_POOMCAL_RTF => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'rtf'),
+            SYNC_POOMCAL_MEETINGSTATUS => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'meetingstatus'),
+            SYNC_POOMCAL_ATTENDEES => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'attendees', Horde_ActiveSync_Message_Base::KEY_TYPE => 'Horde_ActiveSync_Message_Attendee', Horde_ActiveSync_Message_Base::KEY_VALUES => SYNC_POOMCAL_ATTENDEE),
+            SYNC_POOMCAL_BODY => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'body'),
+            SYNC_POOMCAL_BODYTRUNCATED => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'bodytruncated'),
+            SYNC_POOMCAL_EXCEPTIONS => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'exceptions', Horde_ActiveSync_Message_Base::KEY_TYPE => 'Horde_ActiveSync_Message_Exception', Horde_ActiveSync_Message_Base::KEY_VALUES => SYNC_POOMCAL_EXCEPTION),
+            SYNC_POOMCAL_CATEGORIES => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'categories', Horde_ActiveSync_Message_Base::KEY_VALUES => SYNC_POOMCAL_CATEGORY),
+            SYNC_POOMCAL_RESPONSETYPE => array(Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'responsetype'),
+        );
+
+        parent::__construct($mapping, $params);
+    }
+
+    /**
+     * Set the timezone
+     *
+     * @param mixed $date     Either a Horde_Date or timezone descriptor such as
+     *                        America/New_York etc...
+     *
+     */
+    public function setTimezone($date)
+    {
+        if (!($date instanceof Horde_Date)) {
+            $timezone = new Horde_Date(time(), $date);
+        }
+        $offsets = Horde_ActiveSync_Timezone::getOffsetsFromDate($date);
+        $tz = Horde_ActiveSync_Timezone::getSyncTZFromOffsets($offsets);
+        $this->_properties['timezone'] = $tz;
+    }
+
+    /**
+     * Set the appointment's modify timestamp
+     *
+     * @param mixed $timestamp  Horde_Date or a unix timestamp
+     */
+    public function setDTStamp($date)
+    {
+        if (!($date instanceof Horde_Date)) {
+            $date = new Horde_Date($date);
+        }
+        $this->_properties['dtstamp'] = $date;
+    }
+
+    /**
+     * Get the appointment's dtimestamp
+     *
+     */
+    public function getDTStamp()
+    {
+        return $this->_getAttribute('dtstamp');
+    }
+
+    /**
+     * Set the appointment time/duration.
+     *
+     * @param array $timestamp 'start', 'end' or 'duration' (in seconds) or 'allday'
+     */
+    public function setDatetime($datetime = array())
+    {
+        /* Start date is always required */
+        if (empty($datetime['start'])) {
+            throw new InvalidArgumentException('Missing the required start parameter');
+        }
+
+        /* Get or calculate start and end time in local tz */
+        $start = clone($datetime['start']);
+        if (!empty($datetime['end'])) {
+            $end = clone($datetime['end']);
+        } elseif (!empty($datetime['duration'])) {
+            $end = clone($start);
+            $end->sec += $datetime['duration'];
+        } else {
+            $end = clone($start);
+        }
+
+        /*Is this an all day event? */
+        if ($start->hour == 0 &&
+            $start->min == 0 &&
+            $start->sec == 0 &&
+            $end->hour == 23 &&
+            $end->min == 59) {
+
+            $end = new Horde_Date(
+                array('year'  => (int)$end->year,
+                      'month' => (int)$end->month,
+                      'mday'  => (int)$end->mday + 1));
+
+            $this->_properties['alldayevent'] = self::IS_ALL_DAY;
+        } elseif (!empty($datetime['allday'])) {
+            $this->_properties['alldayevent'] = self::IS_ALL_DAY;
+        }
+        $this->_properties['starttime'] = $start;
+        $this->_properties['endtime'] = $end;
+    }
+
+    /**
+     * Get the appointment's time data
+     *
+     * @return array containing 'start', 'end', 'allday'
+     */
+    public function getDatetime()
+    {
+        return array(
+            'start' => $this->_properties['starttime'],
+            'end' => $this->_properties['endtime'],
+            'allday' => !empty($this->_properties['alldayevent']) ? true : false
+        );
+    }
+
+    /**
+     * Set the appointment subject field.
+     *
+     * @param string $subject   UTF-8 string
+     */
+    public function setSubject($subject)
+    {
+        $this->_properties['subject'] = $subject;
+    }
+
+    public function getSubject()
+    {
+        return $this->_getAttribute('subject');
+    }
+
+    /**
+     * Set the appointment uid. Note that this is the PIM's UID value, and not
+     * the value that the server uses for the UID. ActiveSync messages do not
+     * include any server uid value as part of the message natively.
+     *
+     * @param string $uid  The server's uid for this appointment
+     */
+    public function setUid($uid)
+    {
+        $this->_properties['uid'] = $uid;
+    }
+
+    /**
+     * Get the PIM's UID. See not above regarding server UIDs.
+     *
+     * @return string
+     */
+    public function getUid()
+    {
+        return $this->_getAttribute('uid');
+    }
+
+    /**
+     * Because the PIM doesn't pass the server uid as part of the message,
+     * we need to add it manually so the backend can have access to it
+     * when changing this object.
+     *
+     * @param string $uid  The server UID
+     */
+    public function setServerUID($uid)
+    {
+        $this->_getAttribute('serveruid');
+    }
+
+    /**
+     * Obtain the server UID. See note above.
+     *
+     * @return string
+     */
+    public function getServerUID()
+    {
+        return $this->_getAttribute('serveruid');
+    }
+
+    /**
+     * Set the organizer name and/or email
+     *
+     * @param array  'name' and 'email' for this appointment organizer.
+     */
+    public function setOrganizer($organizer)
+    {
+        $this->_properties['organizername'] = !empty($organizer['name'])
+                                                ? $organizer['name']
+                                                : '';
+
+        $this->_properties['organizeremail'] = !empty($organizer['email'])
+                                                ? $organizer['email']
+                                                : '';
+    }
+
+    /**
+     * Get the details for the appointment organizer
+     *
+     * @return array with 'name' and 'email' values
+     */
+    public function getOrganizer()
+    {
+        return array('name' => $this->_getAttribute('organizername'),
+                     'email' => $this->_getAttribute('organizeremail'));
+    }
+
+    /**
+     * Set appointment location field.
+     *
+     * @param string $location
+     */
+    public function setLocation($location)
+    {
+        $this->_properties['location'] = $location;
+    }
+
+    /**
+     * Get the location field
+     *
+     * @return string
+     */
+    public function getLocation()
+    {
+        return $this->_getAttribute('location');
+    }
+
+    /**
+     * Set recurrence information for this appointment
+     *
+     * @param Horde_Date_Recurrence $recurrence
+     */
+    public function setRecurrence(Horde_Date_Recurrence $recurrence)
+    {
+        $r = new Horde_ActiveSync_Message_Recurrence();
+
+        /* Map the type fields */
+        switch ($recurrence->recurType) {
+        case Horde_Date_Recurrence::RECUR_DAILY:
+            $r->type = Horde_ActiveSync_Message_Recurrence::TYPE_DAILY;
+            break;
+        case Horde_Date_Recurrence::RECUR_WEEKLY;
+            $r->type = Horde_ActiveSync_Message_Recurrence::TYPE_WEEKLY;
+            $r->dayofweek = $recurrence->getRecurOnDays();
+            break;
+        case Horde_Date_Recurrence::RECUR_MONTHLY_DATE:
+            $r->type = Horde_ActiveSync_Message_Recurrence::TYPE_MONTHLY;
+            break;
+        case Horde_Date_Recurrence::RECUR_MONTHLY_WEEKDAY;
+            $r->type = Horde_ActiveSync_Message_Recurrence::TYPE_MONTHLY_NTH;
+            $r->dayofweek = $recurrence->getRecurOnDays();
+            break;
+        case Horde_Date_Recurrence::RECUR_YEARLY_DATE:
+            $r->type = Horde_ActiveSync_Message_Recurrence::TYPE_YEARLY;
+            break;
+        case Horde_Date_Recurrence::RECUR_YEARLY_WEEKDAY:
+            $r->type = Horde_ActiveSync_Message_Recurrence::TYPE_YEARLYNTH;
+            $r->dayofweek = $recurrence->getRecurOnDays();
+            break;
+        }
+
+        $this->_properties['recurrence'] = $r;
+    }
+
+    /**
+     * Obtain a recurrence object. Note this returns a Horde_Date_Recurrence
+     * object, not Horde_ActiveSync_Message_Recurrence.
+     *
+     * @return Horde_Date_Recurrence
+     */
+    public function getRecurrence()
+    {
+        if (!$recurrence = $this->_getAttribute('recurrence')) {
+            return false;
+        }
+
+        $rrule = new Horde_Date_Recurrence(new Horde_Date($this->_getAttribute('startdate')));
+
+        /* Map MS AS type field to Horde_Date_Recurrence types */
+        switch ($recurrence->type) {
+        case Horde_ActiveSync_Message_Recurrence::TYPE_DAILY:
+            $rrule->setRecurType(Horde_Date_Recurrence::RECUR_DAILY);
+             break;
+        case Horde_ActiveSync_Message_Recurrence::TYPE_WEEKLY:
+            $rrule->setRecurType(Horde_Date_Recurrence::RECUR_WEEKLY);
+            $rrule->setRecurOnDay($recurrence->dayofweek);
+            break;
+        case Horde_ActiveSync_Message_Recurrence::TYPE_MONTHLY:
+            $rrule->setRecurType(Horde_Date_Recurrence::RECUR_MONTHLY_DATE);
+            break;
+        case Horde_ActiveSync_Message_Recurrence::TYPE_MONTHLY_NTH:
+            $rrule->setRecurType(Horde_Date_Recurrence::RECUR_MONTHLY_WEEKDAY);
+            $rrule->setRecurOnDay($recurrence->dayofweek);
+            break;
+        /* TODO: Not sure about these 'Nth' rules - might need more eyes */
+        case Horde_ActiveSync_Message_Recurrence::TYPE_YEARLY:
+            $rrule->setRecurType(Horde_Date_Recurrence::RECUR_YEARLY_DATE);
+            break;
+        case Horde_ActiveSync_Message_Recurrence::TYPE_YEARLYNTH:
+            $rrule->setRecurType(Horde_Date_Recurrence::RECUR_YEARLY_WEEKDAY);
+            $rrule->setRecurOnDay($recurrence->dayofweek);
+            break;
+        }
+
+        if ($rcnt = $recurrence->occurrences) {
+            $rrule->setRecurCount($rcnt);
+        }
+        if ($runtil = $recurrence->until) {
+            $rrule->setRecurEnd(new Horde_Date($runtil));
+        }
+        if ($interval = $recurrence->interval) {
+            $rrule->setRecurInterval($interval);
+        }
+
+        return $rrule;
+    }
+
+    /**
+     * Add a recurrence exception
+     *
+     * @param Horde_ActiveSync_Message_Exception $exception
+     */
+    public function addException(Horde_ActiveSync_Message_Exception $exception)
+    {
+//        if (!isset($this->_properties['exceptions']) || !is_array($this->_properties['exceptions'])) {
+//            $this->_properties['exceptions'] = array();
+//        }
+        $this->exceptions[] = $exception;
+        //$this->_properties['exceptions'][] = $exception;
+    }
+
+    /**
+     *
+     * @return array  An array of Horde_ActiveSync_Message_Exception objects
+     */
+    public function getExceptions()
+    {
+        return $this->exceptions;
+        //return $this->_getAttribute('exceptions', array());
+    }
+
+    /**
+     * Set the sensitivity level for this appointment.
+     *
+     * Should be one of:
+     *   normal, personal, private, confidential
+     *
+     * @param string $sensitivity
+     */
+    public function setSensitivity($sensitivity)
+    {
+        switch ($sensitivity) {
+        case 'normal':
+            $sensitivity = self::SENSITIVITY_NORMAL;
+            break;
+        case 'personal':
+            $sensitivity = self::SENSITIVITY_PERSONAL;
+            break;
+        case 'private':
+            $sensitivity = self::SENSITIVITY_PRIVATE;
+            break;
+        case 'confidential':
+            $sensitivity = self::SENSITIVITY_CONFIDENTIAL;
+            break;
+        default:
+            return;
+        }
+
+        $this->_properties['sensitivity'] = $sensitivity;
+    }
+
+    /**
+     * Return the sensitivity setting for this appointment
+     *
+     * @return string  One of: normal, personal, private, confidential
+     */
+    public function getSensitivity()
+    {
+        switch ($this->_getAttribute('sensitivity')) {
+        case self::SENSITIVITY_NORMAL:
+            return 'normal';
+            break;
+        case self::SENSITIVITY_PERSONAL:
+            return 'personal';
+            break;
+        case self::SENSITIVITY_PRIVATE:
+            return 'private';
+            break;
+        case self::SENSITIVITY_CONFIDENTIAL:
+            return 'confidential';
+            break;
+        default:
+            return;
+        }
+    }
+
+    /**
+     * Sets the busy status for this appointment
+     *
+     * Should be one of:
+     *   free, tenative, busy, out
+     *
+     *
+     * @param string  $busy  The busy status to use
+     */
+    public function setBusyStatus($busy)
+    {
+        switch ($busy) {
+        case 'free':
+            $busy = self::BUSYSTATUS_FREE;
+            break;
+        case 'tenative':
+            $busy = self::BUSYSTATUS_TENATIVE;
+            break;
+        case 'busy':
+            $busy = self::BUSYSTATUS_BUSY;
+            break;
+        case 'out':
+            $busy = self::BUSYSTATUS_OUT;
+            break;
+        default:
+            return;
+        }
+
+        $this->_properties['busystatus'] = $busy;
+    }
+
+    /**
+     * Return the busy status for this appointment.
+     *
+     * @return string  One of free, tenative, busy, out
+     */
+    public function getBusyStatus()
+    {
+        switch ($this->_getAttribute('busystatus')) {
+        case self::BUSYSTATUS_FREE:
+            return 'free';
+            break;
+        case self::BUSYSTATUS_TENATIVE:
+            return 'tenative';
+            break;
+        case self::BUSYSTATUS_BUSY:
+            return 'busy';
+            break;
+        case self::BUSYSTATUS_OUT:
+            return 'out';
+            break;
+        default:
+            return;
+        }
+    }
+
+    /**
+     * Set user response type. Should be one of:
+     *   none, organizer, tenative, accepted, declined
+     *
+     * @param string $response  The response type
+     */
+    public function setResponseType($response)
+    {
+        switch ($response) {
+        case 'none':
+            $response = self::RESPONSE_NONE;
+            break;
+        case 'organizer':
+            $response = self::RESPONSE_ORGANIZER;
+            break;
+        case 'tenative':
+            $response = self::RESPONSE_TENATIVE;
+            break;
+        case 'accepted':
+            $response = self::RESPONSE_ACCEPTED;
+            break;
+        case 'declined':
+            $response = self::RESPONSE_DECLINED;
+            break;
+        default:
+            return;
+        }
+
+        $this->_properties['responsetype'] = $response;
+    }
+
+    /**
+     * Get response type
+     *
+     * @return string one of: none, organizer, tenatve, accepted, declined
+     */
+    public function getResponseType()
+    {
+        switch ($this->_getAttribute('responsetype')) {
+        case self::RESPONSE_NONE:
+            return 'none';
+            break;
+        case self::RESPONSE_ORGANIZER:
+            return 'organizer';
+            break;
+        case self::RESPONSE_TENATIVE:
+            return 'tenative';
+            break;
+        case self::RESPONSE_ACCEPTED:
+            return 'accepted';
+            break;
+        case self::RESPONSE_DECLINED:
+            return 'declined';
+            break;
+        default:
+            return;
+        }
+    }
+
+    /**
+     * Set reminder for this appointment.
+     *
+     * @param integer $minutes  The number of minutes before appintment to
+     *                          trigger a reminder.
+     */
+    public function setReminder($minutes)
+    {
+        $this->_properties['reminder'] = (int)$minutes;
+    }
+
+    /**
+     *
+     * @return integer  Number of minutes before appointment for notifications.
+     */
+    public function getReminder()
+    {
+        return $this->_getAttribute('reminder');
+    }
+
+    /**
+     * Set the status for this appointment. Should be one of:
+     *   none, meeting, received, canceled, canceledreceived.
+     *
+     * TODO: Not really sure about when these would be used.
+     *
+     *
+     * @param <type> $status
+     */
+    public function setMeetingStatus($status)
+    {
+        switch ($status) {
+        case 'none':
+            $status = self::MEETING_NOT_MEETING;
+            break;
+        case 'meeting':
+            $status = self::MEETING_IS_MEETING;
+            break;
+        case 'received':
+            $status = self::MEETING_RECEIVED;
+            break;
+        case 'canceled':
+            $status = self::MEETING_CANCELLED;
+            break;
+        default:
+            return;
+        }
+
+        $this->_properties['meetingstatus'] = $status;
+    }
+
+    /**
+     *
+     * @return string  One of none, meeting, received, canceled
+     */
+    public function getMeetingStatus()
+    {
+        switch ($this->_getAttribute('meetingstatus')) {
+        case self::MEETING_NOT_MEETING:
+            return 'none';
+            break;
+        case self::MEETING_IS_MEETING:
+            return 'meeting';
+            break;
+        case  self::MEETING_RECEIVED:
+            return 'received';
+            break;
+        case self::MEETING_CANCELLED:
+            return 'canceled';
+            break;
+        default:
+            return;
+        }
+    }
+
+    /**
+     * Add an attendee to this appointment
+     *
+     * @param array $attendee   'name', 'email' for each attendee
+     */
+    public function addAttendee($attendee)
+    {
+        if (!isset($this->_properties['attendees']) || !is_array($this->_properties['attendees'])) {
+            $this->_properties['attendees'] = array();
+        }
+
+        /* Both email and name are REQUIRED if setting an attendee */
+        $this->_properties['attendees'][] = $attendee;
+    }
+
+    /**
+     *
+     * @return array  An array of 'name' and 'email' hashes
+     */
+    public function getAttendees()
+    {
+        return $this->_getAttribute('attendees');
+    }
+
+    /**
+     * TODO
+     *
+     * @param <type> $body
+     */
+    public function setBody($body)
+    {
+
+    }
+
+    public function getBody()
+    {
+
+    }
+
+    /**
+     * Add a category to the appointment
+     *
+     * TODO
+     *
+     * @param string $category
+     */
+    public function addCategory($category)
+    {
+
+    }
+
+    public function getCategory()
+    {
+
+    }
+
+    /**
+     * Return the collection class name the object is for.
+     *
+     * @return string
+     */
+    public function getClass()
+    {
+        return 'Calendar';
+    }
+
+    protected function _getAttribute($name, $default = null)
+    {
+        if (!empty($this->_properties[$name])) {
+            return $this->_properties[$name];
+        } else {
+            return $default;
+        }
+    }
+}
\ No newline at end of file
diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Message/Attendee.php b/framework/ActiveSync/lib/Horde/ActiveSync/Message/Attendee.php
new file mode 100644 (file)
index 0000000..a71db96
--- /dev/null
@@ -0,0 +1,27 @@
+<?php
+/**
+ * Horde_ActiveSync_Message_Attendee class represents a single ActiveSync
+ * Attendee sub-object.
+ *
+ * @copyright 2010 The Horde Project (http://www.horde.org)
+ *
+ * @author Michael J. Rubinsky <mrubinsk@horde.org>
+ * @package Horde_ActiveSync
+ */
+class Horde_ActiveSync_Message_Attendee extends Horde_ActiveSync_Message_Base
+{
+    /**
+     * Const'r
+     *
+     * @param array $params
+     */
+    function __construct($params) {
+        $mapping = array(
+            SYNC_POOMCAL_EMAIL => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'email'),
+            SYNC_POOMCAL_NAME => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'name' )
+        );
+
+        parent::__construct($mapping, $params);
+    }
+
+}
\ No newline at end of file
diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Message/Base.php b/framework/ActiveSync/lib/Horde/ActiveSync/Message/Base.php
new file mode 100644 (file)
index 0000000..fe25350
--- /dev/null
@@ -0,0 +1,384 @@
+<?php
+/**
+ * Horde_ActiveSync_Message_* classes represent a single ActiveSync message
+ * such as a Contact or Appointment. Encoding/Decoding logic taken from the
+ * Z-Push library. Original file header and copyright notice appear below.
+ *
+ * @copyright 2010 The Horde Project (http://www.horde.org)
+ *
+ * @author Michael J. Rubinsky <mrubinsk@horde.org>
+ * @package Horde_ActiveSync
+ */
+/***********************************************
+* File      :   streamer.php
+* Project   :   Z-Push
+* Descr     :   This file handles streaming of
+*                WBXML objects. It must be
+*                subclassed so the internals of
+*                the object can be specified via
+*                $mapping. Basically we set/read
+*                the object variables of the
+*                subclass according to the mappings
+*
+*
+* Created   :   01.10.2007
+*
+* ï¿½ Zarafa Deutschland GmbH, www.zarafaserver.de
+* This file is distributed under GPL v2.
+* Consult LICENSE file for details
+************************************************/
+class Horde_ActiveSync_Message_Base
+{
+
+    /* Attribute Keys */
+    const KEY_ATTRIBUTE = 1;
+    const KEY_VALUES = 2;
+    const KEY_TYPE = 3;
+
+    /* Types */
+    const TYPE_DATE = 1;
+    const TYPE_HEX = 2;
+    const TYPE_DATE_DASHES = 3;
+    const TYPE_MAPI_STREAM = 4;
+
+    /**
+     * Holds the mapping for SYNC_POOMCAL_* -> object properties
+     *
+     * @var array
+     */
+    protected $_mapping;
+
+    /**
+     * Holds property values
+     *
+     * @array
+     */
+    protected $_properties = array();
+
+    /**
+     * Message flags
+     * //FIXME: use accessor methods, make this protected
+     * @var Horde_ActiveSync_FLAG_* constant
+     */
+    public $flags;
+
+    /**
+     * Logger
+     *
+     * @var Horde_Log_Logger
+     */
+    protected $_logger;
+
+    /**
+     * Const'r
+     *
+     * @param array $mapping  A mapping array from constants -> property names
+     * @param array $options  Any addition options the message may require
+     *
+     * @return Horde_ActiveSync_Message_Base
+     */
+    public function __construct($mapping, $options)
+    {
+        $this->_mapping = $mapping;
+        $this->flags = false;
+        if (!empty($options['logger'])) {
+            $this->_logger = $options['logger'];
+        }
+    }
+
+    public function __get($property)
+    {
+        if (!empty($this->_properties[$property])) {
+            return $this->_properties[$property];
+        } else {
+            return '';
+        }
+    }
+
+    public function __set($property, $value)
+    {
+        $this->_properties[$property] = $value;
+    }
+
+    public function __isset($property)
+    {
+        return !empty($this->_properties[$property]);
+    }
+
+    /**
+     * Recursively decodes the WBXML from input stream. This means that if this
+     * message contains complex types (like Appointment.Recuurence for example)
+     * the sub-objects are auto-instantiated and decoded as well. Places the
+     * decoded objects in the local properties array.
+     *
+     * @param Horde_ActiveSync_Wbxml_Decoder  The stream decoder
+     *
+     * @throws Horde_ActiveSync_Exception
+     * @return void
+     */
+    public function decodeStream(Horde_ActiveSync_Wbxml_Decoder &$decoder)
+    {
+        while (1) {
+            $entity = $decoder->getElement();
+
+            if ($entity[Horde_ActiveSync_Wbxml::EN_TYPE] == Horde_ActiveSync_Wbxml::EN_TYPE_STARTTAG) {
+                if (! ($entity[Horde_ActiveSync_Wbxml::EN_FLAGS] & Horde_ActiveSync_Wbxml::EN_FLAGS_CONTENT)) {
+                    $map = $this->_mapping[$entity[Horde_ActiveSync_Wbxml::EN_TAG]];
+                    if (!isset($map[Horde_ActiveSync_Message_Base::KEY_TYPE])) {
+                        $this->$map[Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE] = '';
+                    } elseif ($map[Horde_ActiveSync_Message_Base::KEY_TYPE] == Horde_ActiveSync_Message_Base::TYPE_DATE || $map[Horde_ActiveSync_Message_Base::KEY_TYPE] == Horde_ActiveSync_Message_Base::TYPE_DATE_DASHES ) {
+                        $this->$map[Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE] = '';
+                    }
+                    continue;
+                }
+
+                /* Found start tag */
+                if (!isset($this->_mapping[$entity[Horde_ActiveSync_Wbxml::EN_TAG]])) {
+                    $this->_logDebug('Tag ' . $entity[Horde_ActiveSync_Wbxml::EN_TAG] . ' unexpected in type XML type ' . get_class($this));
+                    throw new Horde_ActiveSync_Exception('Unexpected tag');
+                } else {
+                    $map = $this->_mapping[$entity[Horde_ActiveSync_Wbxml::EN_TAG]];
+
+                    /* Handle arrays of attribute values */
+                    if (isset($map[Horde_ActiveSync_Message_Base::KEY_VALUES])) {
+                        while (1) {
+                            if (!$decoder->getElementStartTag($map[Horde_ActiveSync_Message_Base::KEY_VALUES])) {
+                                break;
+                            }
+                            if (isset($map[Horde_ActiveSync_Message_Base::KEY_TYPE])) {
+                                $decoded = new $map[Horde_ActiveSync_Message_Base::KEY_TYPE];
+                                $decoded->decodeStream($decoder);
+                            } else {
+                                $decoded = $decoder->getElementContent();
+                            }
+
+                            if (!isset($this->$map[Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE])) {
+                                $this->$map[Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE] = array($decoded);
+                            } else {
+                                array_push($this->$map[Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE], $decoded);
+                            }
+
+                            if (!$decoder->getElementEndTag()) {
+                                throw new Horde_ActiveSync_Exception('Missing expected wbxml end tag');
+                            }
+                        }
+
+                        if (!$decoder->getElementEndTag()) {
+                            return false;
+                        }
+                    } else {
+                        /* Handle a simple attribute value */
+                        if (isset($map[Horde_ActiveSync_Message_Base::KEY_TYPE])) {
+                            /* Complex type, decode recursively */
+                            if ($map[Horde_ActiveSync_Message_Base::KEY_TYPE] == Horde_ActiveSync_Message_Base::TYPE_DATE || $map[Horde_ActiveSync_Message_Base::KEY_TYPE] == Horde_ActiveSync_Message_Base::TYPE_DATE_DASHES) {
+                                $decoded = self::_parseDate($decoder->getElementContent());
+                                if (!$decoder->getElementEndTag()) {
+                                    throw new Horde_ActiveSync_Exception('Missing expected wbxml end tag');
+                                }
+                            } elseif ($map[Horde_ActiveSync_Message_Base::KEY_TYPE] == Horde_ActiveSync_Message_Base::TYPE_HEX) {
+                                $decoded = self::hex2bin($decoder->getElementContent());
+                                if (!$decoder->getElementEndTag()) {
+                                   throw new Horde_ActiveSync_Exception('Missing expected wbxml end tag');
+                                }
+                            } else {
+                                $subdecoder = new $map[Horde_ActiveSync_Message_Base::KEY_TYPE]();
+                                if ($subdecoder->decodeStream($decoder) === false) {
+                                    throw new Horde_ActiveSync_Exception('Missing expected wbxml end tag');
+                                }
+
+                                $decoded = $subdecoder;
+                                if (!$decoder->getElementEndTag()) {
+                                    $this->_logError('No end tag for ' . $entity[Horde_ActiveSync_Wbxml::EN_TAG]);
+                                    throw new Horde_ActiveSync_Exception('Missing expected wbxml end tag');
+                                }
+                            }
+                        } else {
+                            /* Simple type, just get content */
+                            $decoded = $decoder->getElementContent();
+                            if ($decoded === false) {
+                                $this->_logError('Unable to get content for ' . $entity[Horde_ActiveSync_Wbxml::EN_TAG]);
+                                throw new Horde_ActiveSync_Exception('Unknown parsing error.');
+                            }
+
+                            if (!$decoder->getElementEndTag()) {
+                                $this->_logError('Unable to get end tag for ' . $entity[Horde_ActiveSync_Wbxml::EN_TAG]);
+                                throw new Horde_ActiveSync_Exception('Missing expected wbxml end tag');
+                            }
+                        }
+                        /* $decoded now contains data object (or string) */
+                        $this->$map[Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE] = $decoded;
+                    }
+                }
+            } elseif ($entity[Horde_ActiveSync_Wbxml::EN_TYPE] == Horde_ActiveSync_Wbxml::EN_TYPE_ENDTAG) {
+                $decoder->_ungetElement($entity);
+                break;
+            } else {
+                $this->_logError('Unexpected content in type');
+                break;
+            }
+        }
+    }
+
+    /**
+     * Decodes a wbxml string into this object's properties.
+     *
+     * @param string $wbxml
+     */
+    public function decode($wbxml)
+    {
+        throw new Horde_ActiveSync_Exception('Not implemented.');
+    }
+
+    /**
+     * Encodes this message object into a wbxml string.
+     *
+     * @return string wbxml string
+     */
+    public function encode()
+    {
+        throw new Horde_ActiveSync_Exception('Not Implemented.');
+    }
+
+    /**
+     * Encodes this object (and any sub-objects) as wbxml to the output stream.
+     * Output is ordered according to $_mapping
+     *
+     * @param Horde_ActiveSync_Wbxml_Encoder $encoder  The wbxml stream encoder
+     *
+     * @return void
+     */
+    public function encodeStream(Horde_ActiveSync_Wbxml_Encoder &$encoder)
+    {
+        foreach ($this->_mapping as $tag => $map) {
+            if (isset($this->$map[Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE])) {
+                /* Variable is available */
+                if (is_object($this->$map[Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE]) && !($this->$map[Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE] instanceof Horde_Date)) {
+                    /* Subobjects can do their own encoding */
+                    $encoder->startTag($tag);
+                    $this->$map[Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE]->encodeStream($encoder);
+                    $encoder->endTag();
+                } elseif (isset($map[Horde_ActiveSync_Message_Base::KEY_VALUES]) && is_array($this->$map[Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE])) {
+                    /* Array of objects */
+                    $encoder->startTag($tag); // Outputs array container (eg Attachments)
+                    foreach ($this->$map[Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE] as $element) {
+                        if (is_object($element)) {
+                            // Outputs object container (eg Attachment)
+                            $encoder->startTag($map[Horde_ActiveSync_Message_Base::KEY_VALUES]);
+                            $element->encodeStream($encoder);
+                            $encoder->endTag();
+                        } else {
+                            if(strlen($element) == 0) {
+                                  // Do not output empty items. Not sure if we
+                                  // should output an empty tag with
+                                  // $encoder->startTag($map[Horde_ActiveSync_Message_Base::KEY_VALUES], false, true);
+                            } else {
+                                $encoder->startTag($map[Horde_ActiveSync_Message_Base::KEY_VALUES]);
+                                $encoder->content($element);
+                                $encoder->endTag();
+                            }
+                        }
+                    }
+                    $encoder->endTag();
+                } else {
+                    /* Simple type */
+                    if (strlen($this->$map[Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE]) == 0) {
+                          // Do not output empty items.
+                          // See above: $encoder->startTag($tag, false, true);
+                        continue;
+                    } else {
+                        $encoder->startTag($tag);
+                    }
+                    if (isset($map[Horde_ActiveSync_Message_Base::KEY_TYPE]) && ($map[Horde_ActiveSync_Message_Base::KEY_TYPE] == Horde_ActiveSync_Message_Base::TYPE_DATE || $map[Horde_ActiveSync_Message_Base::KEY_TYPE] == Horde_ActiveSync_Message_Base::TYPE_DATE_DASHES)) {
+                        if (!empty($this->$map[Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE])) { // don't output 1-1-1970
+                          $encoder->content(self::_formatDate($this->$map[Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE], $map[Horde_ActiveSync_Message_Base::KEY_TYPE]));
+                        }
+                    } elseif (isset($map[Horde_ActiveSync_Message_Base::KEY_TYPE]) && $map[Horde_ActiveSync_Message_Base::KEY_TYPE] == Horde_ActiveSync_Message_Base::TYPE_HEX) {
+                        $encoder->content(bin2hex($this->$map[Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE]));
+                    } elseif (isset($map[Horde_ActiveSync_Message_Base::KEY_TYPE]) && $map[Horde_ActiveSync_Message_Base::KEY_TYPE] == Horde_ActiveSync_Message_Base::TYPE_MAPI_STREAM) {
+                        $encoder->content($this->$map[Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE]);
+                    } else {
+                        $encoder->content($this->$map[Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE]);
+                    }
+                    $encoder->endTag();
+                }
+            }
+        }
+    }
+
+    /**
+     *
+     * @param $message
+     * @return unknown_type
+     */
+    protected function _logDebug($message)
+    {
+        if (!empty($this->_logger)) {
+            $this->_logger->debug($message);
+        }
+    }
+
+    /**
+     *
+     * @param $message
+     * @return unknown_type
+     */
+    protected function _logError($message)
+    {
+        if (!empty($this->_logger)) {
+            $this->_logger->error($message);
+        }
+    }
+
+    /**
+     * Oh yeah. This is beautiful. Exchange outputs date fields differently in
+     * calendar items and emails. We could just always send one or the other,
+     * but unfortunately nokia's 'Mail for exchange' depends on this quirk.
+     * So we have to send a different date type depending on where it's used.
+     *
+     * @param Horde_Date $dt  The datetime to format (assumed to be in local tz)
+     * @param Constant $type  The type to format as (TYPE_DATE or TYPE_DATE_DASHES)
+     *
+     * @return string  The formatted date
+     */
+    static protected function _formatDate($dt, $type)
+    {
+        if ($type == Horde_ActiveSync_Message_Base::TYPE_DATE) {
+            return $dt->setTimezone('UTC')->format('Ymd\THis\Z');
+        } elseif ($type == Horde_ActiveSync_Message_Base::TYPE_DATE_DASHES) {
+            return $dt->setTimezone('UTC')->format('Y-m-d\TH:i:s\.000\Z');
+        }
+    }
+
+    /**
+     * Get a Horde_Date from a timestamp, ensuring it's in the correct format.
+     *
+     * @param string $ts
+     *
+     * @return Horde_Date
+     */
+    static protected function _parseDate($ts)
+    {
+        if (preg_match("/(\d{4})[^0-9]*(\d{2})[^0-9]*(\d{2})T(\d{2})[^0-9]*(\d{2})[^0-9]*(\d{2})(.\d+)?Z/", $ts, $matches)) {
+            return new Horde_Date($ts);
+        }
+
+        throw new Horde_ActiveSync_Exception('Invalid date format');
+    }
+
+    /**
+     * Function which converts a hex entryid to a binary entryid.
+     * @param string @data the hexadecimal string
+     */
+    static private function hex2bin($data)
+    {
+        $len = strlen($data);
+        $newdata = "";
+
+        for($i = 0;$i < $len;$i += 2)
+        {
+            $newdata .= pack("C", hexdec(substr($data, $i, 2)));
+        }
+        return $newdata;
+    }
+
+}
\ No newline at end of file
diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Message/Contact.php b/framework/ActiveSync/lib/Horde/ActiveSync/Message/Contact.php
new file mode 100644 (file)
index 0000000..388b957
--- /dev/null
@@ -0,0 +1,151 @@
+<?php
+/**
+ * Horde_ActiveSync_Message_Contact class represents a single ActiveSync
+ * Contact object.
+ *
+ * @copyright 2010 The Horde Project (http://www.horde.org)
+ *
+ * @author Michael J. Rubinsky <mrubinsk@horde.org>
+ * @package Horde_ActiveSync
+ */
+class Horde_ActiveSync_Message_Contact extends Horde_ActiveSync_Message_Base
+{
+    public $anniversary;
+    public $assistantname;
+    public $assistnamephonenumber;
+    public $birthday;
+    public $body;
+    public $bodysize;
+    public $bodytruncated;
+    public $business2phonenumber;
+    public $businesscity;
+    public $businesscountry;
+    public $businesspostalcode;
+    public $businessstate;
+    public $businessstreet;
+    public $businessfaxnumber;
+    public $businessphonenumber;
+    public $carphonenumber;
+    public $categories = array();
+    public $children = array();
+    public $companyname;
+    public $department;
+    public $email1address;
+    public $email2address;
+    public $email3address;
+    public $fileas;
+    public $firstname;
+    public $home2phonenumber;
+    public $homecity;
+    public $homecountry;
+    public $homepostalcode;
+    public $homestate;
+    public $homestreet;
+    public $homefaxnumber;
+    public $homephonenumber;
+    public $jobtitle;
+    public $lastname;
+    public $middlename;
+    public $mobilephonenumber;
+    public $officelocation;
+    public $othercity;
+    public $othercountry;
+    public $otherpostalcode;
+    public $otherstate;
+    public $otherstreet;
+    public $pagernumber;
+    public $radiophonenumber;
+    public $spouse;
+    public $suffix;
+    public $title;
+    public $webpage;
+    public $yomicompanyname;
+    public $yomifirstname;
+    public $yomilastname;
+    public $rtf;
+    public $picture;
+    public $nickname;
+
+    public function __construct($params = array())
+    {
+        $mapping = array (
+            SYNC_POOMCONTACTS_ANNIVERSARY => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE =>  'anniversary', Horde_ActiveSync_Message_Base::KEY_TYPE => Horde_ActiveSync_Message_Base::TYPE_DATE_DASHES  ),
+            SYNC_POOMCONTACTS_ASSISTANTNAME => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'assistantname'),
+            SYNC_POOMCONTACTS_ASSISTNAMEPHONENUMBER => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'assistnamephonenumber'),
+            SYNC_POOMCONTACTS_BIRTHDAY => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'birthday', Horde_ActiveSync_Message_Base::KEY_TYPE => Horde_ActiveSync_Message_Base::TYPE_DATE_DASHES  ),
+            SYNC_POOMCONTACTS_BODY => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'body'),
+            SYNC_POOMCONTACTS_BODYSIZE => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'bodysize'),
+            SYNC_POOMCONTACTS_BODYTRUNCATED => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'bodytruncated'),
+            SYNC_POOMCONTACTS_BUSINESS2PHONENUMBER => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'business2phonenumber'),
+            SYNC_POOMCONTACTS_BUSINESSCITY => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'businesscity'),
+            SYNC_POOMCONTACTS_BUSINESSCOUNTRY => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'businesscountry'),
+            SYNC_POOMCONTACTS_BUSINESSPOSTALCODE => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'businesspostalcode'),
+            SYNC_POOMCONTACTS_BUSINESSSTATE => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'businessstate'),
+            SYNC_POOMCONTACTS_BUSINESSSTREET => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'businessstreet'),
+            SYNC_POOMCONTACTS_BUSINESSFAXNUMBER => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'businessfaxnumber'),
+            SYNC_POOMCONTACTS_BUSINESSPHONENUMBER => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'businessphonenumber'),
+            SYNC_POOMCONTACTS_CARPHONENUMBER => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'carphonenumber'),
+            SYNC_POOMCONTACTS_CHILDREN => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'children', Horde_ActiveSync_Message_Base::KEY_VALUES => SYNC_POOMCONTACTS_CHILD ),
+            SYNC_POOMCONTACTS_COMPANYNAME => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'companyname'),
+            SYNC_POOMCONTACTS_DEPARTMENT => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'department'),
+            SYNC_POOMCONTACTS_EMAIL1ADDRESS => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'email1address'),
+            SYNC_POOMCONTACTS_EMAIL2ADDRESS => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'email2address'),
+            SYNC_POOMCONTACTS_EMAIL3ADDRESS => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'email3address'),
+            SYNC_POOMCONTACTS_FILEAS => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'fileas'),
+            SYNC_POOMCONTACTS_FIRSTNAME => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'firstname'),
+            SYNC_POOMCONTACTS_HOME2PHONENUMBER => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'home2phonenumber'),
+            SYNC_POOMCONTACTS_HOMECITY => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'homecity'),
+            SYNC_POOMCONTACTS_HOMECOUNTRY => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'homecountry'),
+            SYNC_POOMCONTACTS_HOMEPOSTALCODE => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'homepostalcode'),
+            SYNC_POOMCONTACTS_HOMESTATE => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'homestate'),
+            SYNC_POOMCONTACTS_HOMESTREET => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'homestreet'),
+            SYNC_POOMCONTACTS_HOMEFAXNUMBER => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'homefaxnumber'),
+            SYNC_POOMCONTACTS_HOMEPHONENUMBER => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'homephonenumber'),
+            SYNC_POOMCONTACTS_JOBTITLE => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'jobtitle'),
+            SYNC_POOMCONTACTS_LASTNAME => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'lastname'),
+            SYNC_POOMCONTACTS_MIDDLENAME => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'middlename'),
+            SYNC_POOMCONTACTS_MOBILEPHONENUMBER => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'mobilephonenumber'),
+            SYNC_POOMCONTACTS_OFFICELOCATION => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'officelocation'),
+            SYNC_POOMCONTACTS_OTHERCITY => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'othercity'),
+            SYNC_POOMCONTACTS_OTHERCOUNTRY => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'othercountry'),
+            SYNC_POOMCONTACTS_OTHERPOSTALCODE => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'otherpostalcode'),
+            SYNC_POOMCONTACTS_OTHERSTATE => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'otherstate'),
+            SYNC_POOMCONTACTS_OTHERSTREET => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'otherstreet'),
+            SYNC_POOMCONTACTS_PAGERNUMBER => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'pagernumber'),
+            SYNC_POOMCONTACTS_RADIOPHONENUMBER => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'radiophonenumber'),
+            SYNC_POOMCONTACTS_SPOUSE => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'spouse'),
+            SYNC_POOMCONTACTS_SUFFIX => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'suffix'),
+            SYNC_POOMCONTACTS_TITLE => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'title'),
+            SYNC_POOMCONTACTS_WEBPAGE => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'webpage'),
+            SYNC_POOMCONTACTS_YOMICOMPANYNAME => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'yomicompanyname'),
+            SYNC_POOMCONTACTS_YOMIFIRSTNAME => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'yomifirstname'),
+            SYNC_POOMCONTACTS_YOMILASTNAME => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'yomilastname'),
+            SYNC_POOMCONTACTS_RTF => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'rtf'),
+            SYNC_POOMCONTACTS_PICTURE => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'picture'),
+            SYNC_POOMCONTACTS_CATEGORIES => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'categories', Horde_ActiveSync_Message_Base::KEY_VALUES => SYNC_POOMCONTACTS_CATEGORY ),
+        );
+
+        /* Additional mappings for AS versions >= 2.5 */
+        if (isset($params['protocolversion']) && $params['protocolversion'] >= 2.5) {
+            $mapping += array(
+                SYNC_POOMCONTACTS2_CUSTOMERID => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'customerid'),
+                SYNC_POOMCONTACTS2_GOVERNMENTID => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'governmentid'),
+                SYNC_POOMCONTACTS2_IMADDRESS => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'imaddress'),
+                SYNC_POOMCONTACTS2_IMADDRESS2 => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'imaddress2'),
+                SYNC_POOMCONTACTS2_IMADDRESS3 => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'imaddress3'),
+                SYNC_POOMCONTACTS2_MANAGERNAME => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'managername'),
+                SYNC_POOMCONTACTS2_COMPANYMAINPHONE => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'companymainphone'),
+                SYNC_POOMCONTACTS2_ACCOUNTNAME => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'accountname'),
+                SYNC_POOMCONTACTS2_NICKNAME => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'nickname'),
+                SYNC_POOMCONTACTS2_MMS => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'mms'),
+            );
+        }
+
+        parent::__construct($mapping, $params);
+    }
+
+    public function getClass()
+    {
+        return 'Contacts';
+    }
+}
diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Message/Exception.php b/framework/ActiveSync/lib/Horde/ActiveSync/Message/Exception.php
new file mode 100644 (file)
index 0000000..ea79df6
--- /dev/null
@@ -0,0 +1,52 @@
+<?php
+/**
+ * Horde_ActiveSync_Message_Exception class represents a single exception to a
+ * recurring event. This is basically a Appointment object with some tweaks.
+ * 
+ * @copyright 2010 The Horde Project (http://www.horde.org)
+ *
+ * @author Michael J. Rubinsky <mrubinsk@horde.org>
+ * @package Horde_ActiveSync
+ */
+class Horde_ActiveSync_Message_Exception extends Horde_ActiveSync_Message_Appointment
+{
+    /**
+     * Constructor
+     *
+     * @param array $params
+     *
+     * @return Horde_ActiveSync_Message_Appointment
+     */
+    public function __construct($params = array())
+    {   
+        parent::__construct($params);
+
+        /* Some additional properties for Exceptions */
+        $this->_mapping[SYNC_POOMCAL_EXCEPTIONSTARTTIME] = array(
+            Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'exceptionstarttime',
+            Horde_ActiveSync_Message_Base::KEY_TYPE => Horde_ActiveSync_Message_Base::TYPE_DATE);
+
+        $this->_mapping[SYNC_POOMCAL_DELETED] = array(Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'deleted');
+    }
+
+    /**
+     * Sets the DELETED field on this exception
+     *
+     * @param boolean $flag
+     */
+    public function setDeletedFlag($flag)
+    {
+        $this->_properties['deleted'] = $flag;
+    }
+
+    /**
+     * Exception start time
+     *
+     * @return Horde_Date  The exception's start time
+     */
+    public function getExceptionStartTime()
+    {
+        return $this->_getAttribute('exceptionstarttime');
+    }
+
+}
diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Message/Folder.php b/framework/ActiveSync/lib/Horde/ActiveSync/Message/Folder.php
new file mode 100644 (file)
index 0000000..d592994
--- /dev/null
@@ -0,0 +1,34 @@
+<?php
+/**
+ * Horde_ActiveSync_Message_Folder class represents a single ActiveSync Folder
+ * object.
+ *
+ * @copyright 2010 The Horde Project (http://www.horde.org)
+ *
+ * @author Michael J. Rubinsky <mrubinsk@horde.org>
+ * @package Horde_ActiveSync
+ */
+class Horde_ActiveSync_Message_Folder extends Horde_ActiveSync_Message_Base
+{
+    public $serverid;
+    public $parentid;
+    public $displayname;
+    public $type;
+
+    public function __construct($params = array())
+    {
+        $mapping = array (
+            SYNC_FOLDERHIERARCHY_SERVERENTRYID => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'serverid'),
+            SYNC_FOLDERHIERARCHY_PARENTID => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'parentid'),
+            SYNC_FOLDERHIERARCHY_DISPLAYNAME => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'displayname'),
+            SYNC_FOLDERHIERARCHY_TYPE => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'type')
+        );
+
+        parent::__construct($mapping, $params);
+    }
+
+    public function getClass()
+    {
+        return 'Folders';
+    }
+}
\ No newline at end of file
diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Message/Recurrence.php b/framework/ActiveSync/lib/Horde/ActiveSync/Message/Recurrence.php
new file mode 100644 (file)
index 0000000..f0dae43
--- /dev/null
@@ -0,0 +1,48 @@
+<?php
+/**
+ * Horde_ActiveSync_Message_Recurrence class represents a single ActiveSync
+ * recurrence sub-object.
+ *
+ * @copyright 2010 The Horde Project (http://www.horde.org)
+ *
+ * @author Michael J. Rubinsky <mrubinsk@horde.org>
+ * @package Horde_ActiveSync
+ */
+class Horde_ActiveSync_Message_Recurrence extends Horde_ActiveSync_Message_Base
+{
+    public $type;
+    public $until;
+    public $occurrences;
+    public $interval;
+    public $dayofweek;
+    public $dayofmonth;
+    public $weekofmonth;
+    public $monthofyear;
+
+    /* MS AS Recurrence types */
+    const TYPE_DAILY = 0;
+    const TYPE_WEEKLY = 1;
+    const TYPE_MONTHLY = 2;
+    const TYPE_MONTHLY_NTH = 3;
+    const TYPE_YEARLY = 5;
+    const TYPE_YEARLYNTH = 6;
+
+
+
+    function __construct($params = array())
+    {
+        $mapping = array (
+            SYNC_POOMCAL_TYPE => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'type'),
+            SYNC_POOMCAL_UNTIL => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'until', Horde_ActiveSync_Message_Base::KEY_TYPE => Horde_ActiveSync_Message_Base::TYPE_DATE),
+            SYNC_POOMCAL_OCCURRENCES => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'occurrences'),
+            SYNC_POOMCAL_INTERVAL => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'interval'),
+            SYNC_POOMCAL_DAYOFWEEK => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'dayofweek'),
+            SYNC_POOMCAL_DAYOFMONTH => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'dayofmonth'),
+            SYNC_POOMCAL_WEEKOFMONTH => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'weekofmonth'),
+            SYNC_POOMCAL_MONTHOFYEAR => array (Horde_ActiveSync_Message_Base::KEY_ATTRIBUTE => 'monthofyear')
+        );
+
+        parent::__construct($mapping, $params);
+    }
+
+}
\ No newline at end of file
diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Request/Base.php b/framework/ActiveSync/lib/Horde/ActiveSync/Request/Base.php
new file mode 100644 (file)
index 0000000..4812255
--- /dev/null
@@ -0,0 +1,130 @@
+<?php
+/**
+ * Base class for handling ActiveSync requests
+ *
+ * Copyright 2009 - 2010 The Horde Project (http://www.horde.org)
+ *
+ * @author Michael J. Rubinsky <mrubinsk@horde.org>
+ * @package Horde_ActiveSync
+ */
+/**
+ * Zarafa Deutschland GmbH, www.zarafaserver.de
+ * This file is distributed under GPL v2.
+ * Consult LICENSE file for details
+ */
+abstract class Horde_ActiveSync_Request_Base
+{
+    /**
+     * Driver for communicating with the backend datastore.
+     *
+     * @var Horde_ActiveSync_Driver_Base
+     */
+    protected $_driver;
+
+    /**
+     * Encoder
+     *
+     * @var Horde_ActiveSync_Wbxml_Encoder
+     */
+    protected $_encoder;
+
+    /**
+     * Decoder
+     *
+     * @var Horde_ActiveSync_Wbxml_Decoder
+     */
+    protected $_decoder;
+
+    /**
+     * Request object
+     *
+     * @var Horde_Controller_Request_Http
+     */
+    protected $_request;
+
+    /**
+     * Whether we require provisioned devices.
+     * Valid values are true, false, or loose.
+     * Loose allows devices that don't know about provisioning to continue to
+     * function, but requires devices that are capable to be provisioned.
+     *
+     * @var mixed
+     */
+    protected $_provisioning = false;
+
+    /**
+     * The ActiveSync Version
+     *
+     * @var string
+     */
+    protected $_version;
+
+    /**
+     * The device Id
+     *
+     * @var string
+     */
+    protected $_devId;
+
+    /**
+     * Used to track what error code to send back to PIM on failure
+     *
+     * @var integer
+     */
+    protected $_statusCode = 0;
+
+    protected $_logger;
+
+    /**
+     * Const'r
+     *
+     * @param Horde_ActiveSync_Driver $driver            The backend driver
+     * @param Horde_ActiveSync_Wbxml_Decoder $decoder    The Wbxml decoder
+     * @param Horde_ActiveSync_Wbxml_Endcodder $encdoer  The Wbxml encoder
+     * @param Horde_Controller_Request_Http $request     The request object
+     * @param string $version                            ActiveSync version
+     * @param string $devId                              The PIM device id
+     * @param string $provisioning                       Is provisioning required?
+     *
+     * @return Horde_ActiveSync
+     */
+    public function __construct(Horde_ActiveSync_Driver_Base $driver,
+                                Horde_ActiveSync_Wbxml_Decoder $decoder,
+                                Horde_ActiveSync_Wbxml_Encoder $encoder,
+                                Horde_Controller_Request_Http $request,
+                                $version, $devId, $provisioning)
+    {
+        /* Backend driver */
+        $this->_driver = $driver;
+
+        /* Wbxml handlers */
+        $this->_encoder = $encoder;
+        $this->_decoder = $decoder;
+
+        /* The http request */
+        $this->_request = $request;
+
+        /* Protocol Version */
+        $this->_version = $version;
+
+        /* Device Id */
+        $this->_devId = $devId;
+
+        /* Provisioning support */
+        $this->_provisioning = $provisioning;
+
+        /* Logger */
+        $this->_logger;
+    }
+
+    public function setLogger(Horde_Log_Logger $logger) {
+        $this->_logger = $logger;
+    }
+    /**
+     *
+     * @param string $version
+     * @param string $devId
+     */
+    abstract public function handle(Horde_ActiveSync $activeSync);
+
+}
\ No newline at end of file
diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Request/FolderSync.php b/framework/ActiveSync/lib/Horde/ActiveSync/Request/FolderSync.php
new file mode 100644 (file)
index 0000000..7a59b46
--- /dev/null
@@ -0,0 +1,214 @@
+<?php
+/**
+ * ActiveSync Handler for FOLDERSYNC requests
+ *
+ * Copyright 2009 - 2010 The Horde Project (http://www.horde.org)
+ *
+ * @author Michael J. Rubinsky <mrubinsk@horde.org>
+ * @package Horde_ActiveSync
+ */
+/**
+ * Zarafa Deutschland GmbH, www.zarafaserver.de
+ * This file is distributed under GPL v2.
+ * Consult LICENSE file for details
+ */
+class Horde_ActiveSync_Request_FolderSync extends Horde_ActiveSync_Request_Base
+{
+    /* SYNC Status response codes */
+    const STATUS_SUCCESS = 1;
+    const STATUS_SERVERERROR = 6;  // Should probably return to synckey 0?
+    const STATUS_TIMEOUT = 8;
+    const STATUS_KEYMISM = 9;
+    const STATUS_PROTOERR = 10;
+
+    public function handle(Horde_ActiveSync $activeSync)
+    {
+        /* Be optomistic */
+        $this->_statusCode = self::STATUS_SUCCESS;
+        $this->_logger->info('[Horde_ActiveSync::handleFolderSync] Beginning FOLDERSYNC');
+
+        /* Maps serverid -> clientid for items that are received from the PIM */
+        $map = array();
+
+        /* Start parsing input */
+        if (!$this->_decoder->getElementStartTag(SYNC_FOLDERHIERARCHY_FOLDERSYNC)) {
+            $this->_logger->err('[Horde_ActiveSync::handleFolderSync] No input to parse');
+            $this->_statusCode = self::STATUS_PROTOERR;
+            $this->_handleError();
+            exit;
+        }
+
+        /* Get the current synckey from PIM */
+        if (!$this->_decoder->getElementStartTag(SYNC_FOLDERHIERARCHY_SYNCKEY)) {
+            $this->_logger->err('[Horde_ActiveSync::handleFolderSync] No input to parse');
+            $this->_statusCode = self::STATUS_PROTOERR;
+            $this->_handleError();
+            exit;
+        }
+        $synckey = $this->_decoder->getElementContent();
+        if (!$this->_decoder->getElementEndTag()) {
+            $this->_logger->err('[Horde_ActiveSync::handleFolderSync] No input to parse');
+            $this->_statusCode = self::STATUS_PROTOERR;
+            $this->_handleError();
+            exit;
+        }
+        $this->_logger->debug('[Horde_ActiveSync::handleFolderSync] syncKey: ' . $synckey);
+
+        /* Initialize state engine */
+        $state = &$this->_driver->getStateObject(array('synckey' => $synckey));
+        try {
+            /* Get folders that we know about already */
+            $state->loadState($synckey);
+        } catch (Horde_ActiveSync_Exception $e) {
+            $this->_statusCode = self::STATUS_KEYMISM;
+            $this->_handleError();
+            exit;
+        }
+        $seenfolders = $state->getKnownFolders();
+
+        /* Get new synckey to send back */
+        $newsynckey = $state->getNewSyncKey($synckey);
+        $this->_logger->debug('[Horde_ActiveSync::handleFolderSync] newSyncKey: ' . $newsynckey);
+
+        /* Deal with folder hierarchy changes */
+        if ($this->_decoder->getElementStartTag(SYNC_FOLDERHIERARCHY_CHANGES)) {
+            // Ignore <Count> if present
+            if ($this->_decoder->getElementStartTag(SYNC_FOLDERHIERARCHY_COUNT)) {
+                $this->_decoder->getElementContent();
+                if (!$this->_decoder->getElementEndTag()) {
+                    $this->_statusCode = self::STATUS_PROTOERR;
+                    $this->_handleError();
+                    exit;
+                }
+            }
+
+            /* Process the incoming changes to folders */
+            $element = $this->_decoder->getElement();
+            if ($element[Horde_ActiveSync_Wbxml::EN_TYPE] != Horde_ActiveSync_Wbxml::EN_TYPE_STARTTAG) {
+                $this->_statusCode = self::STATUS_PROTOERR;
+                $this->_handleError();
+                exit;
+            }
+
+            /* Configure importer with last state */
+            $importer = $this->_driver->getImporter();
+            $importer->init($state, false, $synckey);
+
+            while (1) {
+                $folder = new Horde_ActiveSync_Message_Folder(array('logger' => $this->_logger));
+                if (!$folder->decodeStream($this->_decoder)) {
+                    break;
+                }
+
+                switch ($element[Horde_ActiveSync_Wbxml::EN_TAG]) {
+                case SYNC_ADD:
+                case SYNC_MODIFY:
+                    $serverid = $importer->ImportFolderChange($folder);
+                    break;
+                case SYNC_REMOVE:
+                    $serverid = $importer->ImportFolderDeletion($folder);
+                    break;
+                }
+
+                /* Update the map */
+                if ($serverid) {
+                    // FIXME: Yet Another property used, but never defined
+                    $map[$serverid] = $folder->clientid;
+                }
+            }
+
+            if (!$this->_decoder->getElementEndTag()) {
+                $this->_statusCode = self::STATUS_PROTOERR;
+                $this->_handleError();
+                exit;
+            }
+        }
+
+        if (!$this->_decoder->getElementEndTag()) {
+            $this->_statusCode = self::STATUS_PROTOERR;
+            $this->_handleError();
+            exit;
+        }
+
+        /* Start sending server -> PIM changes */
+        $this->_logger->debug('[Horde_ActiveSync::handleFolderSync] Preparing to send changes to PIM');
+
+        // The $importer caches all imports in-memory, so we can send a change
+        // count before sending the actual data. As the amount of data done in
+        // this operation is rather low, this is not memory problem. Note that
+        // this is not done when sync'ing messages - we let the exporter write
+        // directly to WBXML.
+        // TODO: Combine all these import caches into a single Class
+        $importer = new Horde_ActiveSync_HierarchyCache();
+        $exporter = $this->_driver->GetExporter();
+        $exporter->init($state, $importer, array('synckey' => $synckey));
+
+        /* Perform the actual sync operation */
+        while(is_array($exporter->syncronize()));
+
+        // Output our WBXML reply now
+        $this->_encoder->StartWBXML();
+
+        $this->_encoder->startTag(SYNC_FOLDERHIERARCHY_FOLDERSYNC);
+
+        $this->_encoder->startTag(SYNC_FOLDERHIERARCHY_STATUS);
+        $this->_encoder->content($this->_statusCode);
+        $this->_encoder->endTag();
+
+        $this->_encoder->startTag(SYNC_FOLDERHIERARCHY_SYNCKEY);
+        $this->_encoder->content($newsynckey);
+        $this->_encoder->endTag();
+
+        $this->_encoder->startTag(SYNC_FOLDERHIERARCHY_CHANGES);
+
+        $this->_encoder->startTag(SYNC_FOLDERHIERARCHY_COUNT);
+        $this->_encoder->content($importer->count);
+        $this->_encoder->endTag();
+
+        if (count($importer->changed) > 0) {
+            foreach ($importer->changed as $folder) {
+                if (isset($folder->serverid) && in_array($folder->serverid, $seenfolders)) {
+                    $this->_encoder->startTag(SYNC_FOLDERHIERARCHY_UPDATE);
+                } else {
+                    $this->_encoder->startTag(SYNC_FOLDERHIERARCHY_ADD);
+                }
+                $folder->encodeStream($this->_encoder);
+                $this->_encoder->endTag();
+            }
+        }
+
+        if (count($importer->deleted) > 0) {
+            foreach ($importer->deleted as $folder) {
+                $this->_encoder->startTag(SYNC_FOLDERHIERARCHY_REMOVE);
+                $this->_encoder->startTag(SYNC_FOLDERHIERARCHY_SERVERENTRYID);
+                $this->_encoder->content($folder);
+                $this->_encoder->endTag();
+                $this->_encoder->endTag();
+            }
+        }
+
+        $this->_encoder->endTag();
+        $this->_encoder->endTag();
+
+        /* Save the state as well as the known folder cache */
+        $state->setNewSyncKey($newsynckey);
+        $state->save();
+
+        return true;
+    }
+
+    /**
+     * Helper function for sending error responses
+     *
+     */
+    private function _handleError()
+    {
+        $this->_encoder->StartWBXML();
+        $this->_encoder->startTag(SYNC_FOLDERHIERARCHY_FOLDERSYNC);
+        $this->_encoder->startTag(SYNC_FOLDERHIERARCHY_STATUS);
+        $this->_encoder->content($this->_statusCode);
+        $this->_encoder->endTag();
+        $this->_encoder->endTag();
+    }
+
+}
\ No newline at end of file
diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Request/Options.php b/framework/ActiveSync/lib/Horde/ActiveSync/Request/Options.php
new file mode 100644 (file)
index 0000000..b28cea8
--- /dev/null
@@ -0,0 +1,26 @@
+<?php
+/**
+ * ActiveSync Handler for OPTIONS requests
+ *
+ * Copyright 2009 - 2010 The Horde Project (http://www.horde.org)
+ *
+ * @author Michael J. Rubinsky <mrubinsk@horde.org>
+ * @package Horde_ActiveSync
+ */
+/**
+ * Zarafa Deutschland GmbH, www.zarafaserver.de
+ * This file is distributed under GPL v2.
+ * Consult LICENSE file for details
+ */
+class Horde_ActiveSync_Request_Options extends Horde_ActiveSync_Request_Base
+{
+    public function handle(Horde_ActiveSync $activeSync)
+    {
+        Horde_ActiveSync::activeSyncHeader();
+        Horde_ActiveSync::versionHeader();
+        Horde_ActiveSync::commandsHeader();
+
+        return true;
+    }
+
+}
diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Request/Ping.php b/framework/ActiveSync/lib/Horde/ActiveSync/Request/Ping.php
new file mode 100644 (file)
index 0000000..162f0c3
--- /dev/null
@@ -0,0 +1,183 @@
+<?php
+/**
+ * ActiveSync Handler for PING requests
+ *
+ * Copyright 2009 - 2010 The Horde Project (http://www.horde.org)
+ *
+ * @author Michael J. Rubinsky <mrubinsk@horde.org>
+ * @package Horde_ActiveSync
+ */
+/**
+ * Zarafa Deutschland GmbH, www.zarafaserver.de
+ * This file is distributed under GPL v2.
+ * Consult LICENSE file for details
+ */
+class Horde_ActiveSync_Request_Ping extends Horde_ActiveSync_Request_Base
+{
+    const STATUS_NOCHANGES = 1;
+    const STATUS_NEEDSYNC = 2;
+    const STATUS_MISSING = 3;
+    const STATUS_PROTERROR = 4;
+    // Hearbeat out of bounds (TODO)
+    const STATUS_HBOUTOFBOUNDS = 5;
+
+    // Requested more then the max folders (TODO)
+    const STATUS_MAXFOLDERS = 6;
+
+    // Folder sync is required, hierarchy out of date.
+    const STATUS_FOLDERSYNCREQD = 7;
+    const STATUS_SERVERERROR = 8;
+
+    /**
+     * Handle a PING command from the PIM. Ping is sent periodically by the PIM
+     * to tell the server what folders we are interested in monitoring for
+     * changes. If no changes are detected by the server during the 'heartbeat'
+     * interval, the server sends back a status of 1 to indicate heartbeat
+     * expired and the client should re-issue the PING command. If a change
+     * has been found, the client is sent a 2 status and should then issue a
+     * SYNC command.
+     *
+     * @return boolean
+     */
+    public function handle(Horde_ActiveSync $activeSync)
+    {
+        // FIXME
+        $timeout = 3;
+        $this->_logger->info('Ping received for: ' . $this->_devId);
+
+        /* Glass half full kinda guy... */
+        $this->_statusCode = self::STATUS_NOCHANGES;
+
+        /* Initialize the state machine */
+        $state = &$this->_driver->getStateObject();
+
+        /* See if we have an existing PING state. Need to do this here, before
+         * we read in the PING request since the PING request is allowed to omit
+         * sections if they have been sent previously */
+        $collections = array_values($state->initPingState($this->_devId));
+        $lifetime = $state->getPingLifetime();
+
+        /* Build the $collections array if we receive request from PIM */
+        if ($this->_decoder->getElementStartTag(SYNC_PING_PING)) {
+            $this->_logger->debug('Ping init');
+            if ($this->_decoder->getElementStartTag(SYNC_PING_LIFETIME)) {
+                $lifetime = $this->_decoder->getElementContent();
+                $state->setPingLifetime($lifetime);
+                $this->_decoder->getElementEndTag();
+            }
+
+            if ($this->_decoder->getElementStartTag(SYNC_PING_FOLDERS)) {
+                $collections = array();
+                while ($this->_decoder->getElementStartTag(SYNC_PING_FOLDER)) {
+                    $collection = array();
+                    if ($this->_decoder->getElementStartTag(SYNC_PING_SERVERENTRYID)) {
+                        $collection['id'] = $this->_decoder->getElementContent();
+                        $this->_decoder->getElementEndTag();
+                    }
+                    if ($this->_decoder->getElementStartTag(SYNC_PING_FOLDERTYPE)) {
+                        $collection['class'] = $this->_decoder->getElementContent();
+                        $this->_decoder->getElementEndTag();
+                    }
+
+                    $this->_decoder->getElementEndTag();
+                    array_push($collections, $collection);
+                }
+
+                if (!$this->_decoder->getElementEndTag()) {
+                    $this->_statusCode = self::STATUS_PROTERROR;
+                    return false;
+                }
+            }
+
+            if (!$this->_decoder->getElementEndTag()) {
+                $this->_statusCode = self::STATUS_PROTERROR;
+                return false;
+            }
+        }
+
+        $changes = array();
+        $dataavailable = false;
+
+        /* Start waiting for changes, but only if we don't have any errors */
+        if ($this->_statusCode == self::STATUS_NOCHANGES) {
+            $this->_logger->info(sprintf('Waiting for changes... (lifetime %d)', $lifetime));
+            // FIXME
+            //for ($n = 0; $n < $lifetime / $timeout; $n++) {
+            for ($n = 0; $n < 10; $n++) {
+                //check the remote wipe status
+                if ($this->_provisioning === true) {
+                    $rwstatus = $this->_driver->getDeviceRWStatus($this->_devId);
+                    if ($rwstatus == SYNC_PROVISION_RWSTATUS_PENDING || $rwstatus == SYNC_PROVISION_RWSTATUS_WIPED) {
+                        $this->_statusCode = self::STATUS_FOLDERSYNCREQD;
+                        break;
+                    }
+                }
+
+                if (count($collections) == 0) {
+                    $this->_logger->err('0 collections');
+                    $this->_statusCode = self::STATUS_MISSING;
+                    break;
+                }
+
+                for ($i = 0; $i < count($collections); $i++) {
+                    $collection = $collections[$i];
+                    // Make sure we have the synckey (which is the devid for
+                    // PING requests.
+                    $collection['synckey'] = $this->_devId;
+                    $exporter = $this->_driver->getExporter();
+                    $state->loadPingCollectionState($collection);
+                    try {
+                        $exporter->init($state, false, $collection);
+                    } catch (Horde_ActiveSync_Exception $e) {
+                        /* Stop ping if exporter cannot be configured */
+                        $this->_logger->err('Ping error: Exporter can not be configured. Waiting 30 seconds before ping is retried.');
+                        $n = $lifetime/ $timeout;
+                        sleep(30);
+                        break;
+                    }
+
+                    $changecount = $exporter->GetChangeCount();
+                    if ($changecount > 0) {
+                        $dataavailable = true;
+                        $changes[$collection['id']] = $changecount;
+                        $this->_statusCode = self::STATUS_NEEDSYNC;
+                    }
+
+                    // Update the state, but don't bother with the backend since we
+                    // are not updating any data.
+                    while (is_array($exporter->syncronize(BACKEND_DISCARD_DATA)));
+                }
+
+                if ($dataavailable) {
+                    $this->_logger->info('Found changes');
+                    break;
+                }
+                sleep($timeout);
+            }
+        }
+        $this->_logger->debug('Starting StartWBXML output');
+        $this->_encoder->StartWBXML();
+
+        $this->_encoder->startTag(SYNC_PING_PING);
+
+        $this->_encoder->startTag(SYNC_PING_STATUS);
+        $this->_encoder->content($this->_statusCode);
+        $this->_encoder->endTag();
+
+        $this->_encoder->startTag(SYNC_PING_FOLDERS);
+        foreach ($collections as $collection) {
+            if (isset($changes[$collection['id']])) {
+                $this->_encoder->startTag(SYNC_PING_FOLDER);
+                $this->_encoder->content($collection['id']);
+                $this->_encoder->endTag();
+            }
+        }
+        $this->_encoder->endTag();
+
+        $this->_encoder->endTag();
+
+        $state->savePingState();
+
+        return true;
+    }
+}
\ No newline at end of file
diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Request/Provision.php b/framework/ActiveSync/lib/Horde/ActiveSync/Request/Provision.php
new file mode 100644 (file)
index 0000000..86cb7b7
--- /dev/null
@@ -0,0 +1,173 @@
+<?php
+/**
+ * Handle PROVISION requests
+ *
+ * Logic adapted from Z-Push, original copyright notices below.
+ *
+ * Copyright 2009 - 2010 The Horde Project (http://www.horde.org)
+ *
+ * @author Michael J. Rubinsky <mrubinsk@horde.org>
+ * @package Horde_ActiveSync
+ */
+/**
+ * Zarafa Deutschland GmbH, www.zarafaserver.de
+ * This file is distributed under GPL v2.
+ * Consult LICENSE file for details
+ */
+class Horde_ActiveSync_Request_Provision extends Horde_ActiveSync_Request_Base
+{
+
+    public function handle(Horde_ActiveSync $activeSync)
+    {
+        $policykey = $activeSync->getPolicyKey();
+
+        $status = SYNC_PROVISION_STATUS_SUCCESS;
+        $phase2 = true;
+        if (!$this->_decoder->getElementStartTag(SYNC_PROVISION_PROVISION)) {
+            return false;
+        }
+
+        //handle android remote wipe.
+        if ($this->_decoder->getElementStartTag(SYNC_PROVISION_REMOTEWIPE)) {
+            if (!$this->_decoder->getElementStartTag(SYNC_PROVISION_STATUS)) {
+                return false;
+            }
+
+            $status = $this->_decoder->getElementContent();
+
+            if (!$this->_decoder->getElementEndTag()) {
+                return false;
+            }
+
+            if (!$this->_decoder->getElementEndTag()) {
+                return false;
+            }
+        } else {
+            if (!$this->_decoder->getElementStartTag(SYNC_PROVISION_POLICIES)) {
+                return false;
+            }
+
+            if (!$this->_decoder->getElementStartTag(SYNC_PROVISION_POLICY)) {
+                return false;
+            }
+
+            if (!$this->_decoder->getElementStartTag(SYNC_PROVISION_POLICYTYPE)) {
+                return false;
+            }
+
+            $policytype = $this->_decoder->getElementContent();
+            if ($policytype != 'MS-WAP-Provisioning-XML') {
+                $status = SYNC_PROVISION_STATUS_SERVERERROR;
+            }
+            if (!$this->_decoder->getElementEndTag()) {//policytype
+                return false;
+            }
+
+            if ($this->_decoder->getElementStartTag(SYNC_PROVISION_POLICYKEY)) {
+                // This should be Phase 3 of the Provision conversation...
+                // We get the intermediate policy key sent back from the client.
+                // TODO: Still need to verify the key once we have some kind of
+                // storage for it.
+                $policykey = $this->_decoder->getElementContent();
+                if (!$this->_decoder->getElementEndTag()) {
+                    return false;
+                }
+
+                if (!$this->_decoder->getElementStartTag(SYNC_PROVISION_STATUS)) {
+                    return false;
+                }
+
+                $status = $this->_decoder->getElementContent();
+                //do status handling
+                $status = SYNC_PROVISION_STATUS_SUCCESS;
+
+                if (!$this->_decoder->getElementEndTag()) {
+                    return false;
+                }
+                $phase2 = false;
+            }
+
+            if (!$this->_decoder->getElementEndTag()) {//policy
+                return false;
+            }
+
+            if (!$this->_decoder->getElementEndTag()) {//policies
+                return false;
+            }
+
+            if ($this->_decoder->getElementStartTag(SYNC_PROVISION_REMOTEWIPE)) {
+                if (!$this->_decoder->getElementStartTag(SYNC_PROVISION_STATUS)) {
+                    return false;
+                }
+
+                $status = $this->_decoder->getElementContent();
+                if (!$this->_decoder->getElementEndTag()) {
+                    return false;
+                }
+
+                if (!$this->_decoder->getElementEndTag()) {
+                    return false;
+                }
+            }
+        }
+
+        if (!$this->_decoder->getElementEndTag()) {//provision
+            return false;
+        }
+        $this->_encoder->StartWBXML();
+
+        // End of Phase 3 - We create the "final" policy key, store it, then
+        // send it to the client.
+        if (!$phase2) {
+            $policykey = $this->_driver->generatePolicyKey();
+            $this->_driver->setPolicyKey($policykey, $this->_devId);
+        }
+
+        $this->_encoder->startTag(SYNC_PROVISION_PROVISION);
+
+        $this->_encoder->startTag(SYNC_PROVISION_STATUS);
+        $this->_encoder->content($status);
+        $this->_encoder->endTag();
+
+        $this->_encoder->startTag(SYNC_PROVISION_POLICIES);
+        $this->_encoder->startTag(SYNC_PROVISION_POLICY);
+
+        $this->_encoder->startTag(SYNC_PROVISION_POLICYTYPE);
+        $this->_encoder->content($policytype);
+        $this->_encoder->endTag();
+        $this->_encoder->startTag(SYNC_PROVISION_STATUS);
+        $this->_encoder->content($status);
+        $this->_encoder->endTag();
+        $this->_encoder->startTag(SYNC_PROVISION_POLICYKEY);
+        $this->_encoder->content($policykey);
+        $this->_encoder->endTag();
+        if ($phase2) {
+            // If we are in Phase 2, send the security policies.
+            // TODO: Configure this!
+            $this->_encoder->startTag(SYNC_PROVISION_DATA);
+            if ($policytype == 'MS-WAP-Provisioning-XML') {
+                // Set 4131 to 0 to require a PIN, 4133
+                $this->_encoder->content('<wap-provisioningdoc><characteristic type="SecurityPolicy"><parm name="4131" value="1"/><parm name="4133" value="0"/></characteristic></wap-provisioningdoc>');
+            } else {
+                $this->_logger->err('Wrong policy type');
+                return false;
+            }
+            $this->_encoder->endTag();//data
+        }
+        $this->_encoder->endTag();//policy
+        $this->_encoder->endTag(); //policies
+        $rwstatus = $this->_driver->getDeviceRWStatus($this->_devId);
+
+        //wipe data if status is pending or wiped
+        if ($rwstatus == SYNC_PROVISION_RWSTATUS_PENDING || $rwstatus == SYNC_PROVISION_RWSTATUS_WIPED) {
+            $this->_encoder->startTag(SYNC_PROVISION_REMOTEWIPE, false, true);
+            $this->_driver->setDeviceRWStatus($this->_devId, SYNC_PROVISION_RWSTATUS_WIPED);
+            //$rwstatus = SYNC_PROVISION_RWSTATUS_WIPED;
+        }
+
+        $this->_encoder->endTag();//provision
+
+        return true;
+    }
+
+}
\ No newline at end of file
diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Request/Sync.php b/framework/ActiveSync/lib/Horde/ActiveSync/Request/Sync.php
new file mode 100644 (file)
index 0000000..257e71b
--- /dev/null
@@ -0,0 +1,529 @@
+<?php
+/**
+ * ActiveSync Handler for SYNC requests
+ *
+ * Copyright 2009 - 2010 The Horde Project (http://www.horde.org)
+ *
+ * @author Michael J. Rubinsky <mrubinsk@horde.org>
+ * @package Horde_ActiveSync
+ */
+/**
+ * Zarafa Deutschland GmbH, www.zarafaserver.de
+ * This file is distributed under GPL v2.
+ * Consult LICENSE file for details
+ */
+class Horde_ActiveSync_Request_Sync extends Horde_ActiveSync_Request_Base
+{
+    const STATUS_SUCCESS = 1;
+    const STATUS_VERSIONMISM = 2;
+    const STATUS_KEYMISM = 3;
+    const STATUS_PROTERROR = 4;
+    const STATUS_SERVERERROR = 5;
+
+    /**
+     * Handle the sync request
+     *
+     * @return boolean
+     * @throws Horde_ActiveSync_Exception
+     */
+    public function handle(Horde_ActiveSync $activeSync)
+    {
+        $this->_logger->info('[Horde_ActiveSync::handleSync] Handling SYNC command.');
+
+        /* Be optimistic */
+        $this->_statusCode = self::STATUS_SUCCESS;
+
+        /* Contains all containers requested */
+        $collections = array();
+
+        /* Start decoding request */
+        // FIXME: Need to figure out the proper response structure for errors
+        // that occur this early
+        if (!$this->_decoder->getElementStartTag(SYNC_SYNCHRONIZE)) {
+            throw new Horde_ActiveSync_Exception('Protocol error');
+        }
+        if (!$this->_decoder->getElementStartTag(SYNC_FOLDERS)) {
+            throw new Horde_ActiveSync_Exception('Protocol error');
+        }
+
+        while ($this->_statusCode == self::STATUS_SUCCESS &&
+               $this->_decoder->getElementStartTag(SYNC_FOLDER)) {
+
+            $collection = array();
+            $collection['truncation'] = SYNC_TRUNCATION_ALL;
+            $collection['clientids'] = array();
+            $collection['fetchids'] = array();
+
+            if (!$this->_decoder->getElementStartTag(SYNC_FOLDERTYPE)) {
+                throw new Horde_ActiveSync_Exception('Protocol error');
+            }
+
+            $collection['class'] = $this->_decoder->getElementContent();
+            $this->_logger->debug('[Horde_ActiveSync::handleSync] Folder class: ' . $collection['class']);
+            if (!$this->_decoder->getElementEndTag()) {
+                throw new Horde_ActiveSync_Exception('Protocol error');
+            }
+
+            if (!$this->_decoder->getElementStartTag(SYNC_SYNCKEY)) {
+                throw new Horde_ActiveSync_Exception('Protocol error');
+            }
+            $collection['synckey'] = $this->_decoder->getElementContent();
+            if (!$this->_decoder->getElementEndTag()) {
+                throw new Horde_ActiveSync_Exception('Protocol error');
+            }
+
+            if ($this->_decoder->getElementStartTag(SYNC_FOLDERID)) {
+                $collection['id'] = $this->_decoder->getElementContent();
+                $this->_logger->debug('[Horde_ActiveSync::handleSync] Folder collectionid: ' . $collection['id']);
+                if (!$this->_decoder->getElementEndTag()) {
+                    throw new Horde_ActiveSync_Exception('Protocol error');
+                }
+            }
+
+            /* Looks like we ignore the SYNC_SUPPORTED Tag? */
+            // @TODO: This needs to be captured and stored in the state so we
+            // can correctly support ghosted properties
+            if ($this->_decoder->getElementStartTag(SYNC_SUPPORTED)) {
+                // SUPPORTED only allowed on initial sync request
+                if ($collection['synckey'] != 0) {
+                    $this->_statusCode = self::STATUS_PROTERROR;
+                    $this->_handleError($collection);
+                    exit;
+                }
+                while (1) {
+                    $el = $this->_decoder->getElement();
+                    if ($el[Horde_ActiveSync_Wbxml::EN_TYPE] == Horde_ActiveSync_Wbxml::EN_TYPE_ENDTAG) {
+                        break;
+                    }
+                }
+            }
+
+            if ($this->_decoder->getElementStartTag(SYNC_DELETESASMOVES)) {
+                $collection["deletesasmoves"] = true;
+            }
+
+            if ($this->_decoder->getElementStartTag(SYNC_GETCHANGES)) {
+                $collection['getchanges'] = true;
+            }
+
+            if ($this->_decoder->getElementStartTag(SYNC_WINDOWSIZE)) {
+                $collection['windowsize'] = $this->_decoder->getElementContent();
+                if (!$this->_decoder->getElementEndTag()) {
+                    $this->_statusCode = self::STATUS_PROTERROR;
+                    $this->_handleError($collection);
+                    exit;
+                }
+            }
+
+            if ($this->_decoder->getElementStartTag(SYNC_OPTIONS)) {
+                while(1) {
+                    if ($this->_decoder->getElementStartTag(SYNC_FILTERTYPE)) {
+                        $collection['filtertype'] = $this->_decoder->getElementContent();
+                        if (!$this->_decoder->getElementEndTag()) {
+                            $this->_statusCode = self::STATUS_PROTERROR;
+                            $this->_handleError($collection);
+                            exit;
+                        }
+                    }
+                    if ($this->_decoder->getElementStartTag(SYNC_TRUNCATION)) {
+                        $collection['truncation'] = $this->_decoder->getElementContent();
+                        if (!$this->_decoder->getElementEndTag()) {
+                            $this->_statusCode = self::STATUS_PROTERROR;
+                            $this->_handleError($collection);
+                            exit;
+                        }
+                    }
+                    if ($this->_decoder->getElementStartTag(SYNC_RTFTRUNCATION)) {
+                        $collection['rtftruncation'] = $this->_decoder->getElementContent();
+                        if (!$this->_decoder->getElementEndTag()) {
+                            $this->_statusCode = self::STATUS_PROTERROR;
+                            $this->_handleError($collection);
+                            exit;
+                        }
+                    }
+
+                    if ($this->_decoder->getElementStartTag(SYNC_MIMESUPPORT)) {
+                        $collection['mimesupport'] = $this->_decoder->getElementContent();
+                        if (!$this->_decoder->getElementEndTag()) {
+                            $this->_statusCode = self::STATUS_PROTERROR;
+                            $this->_handleError($collection);
+                            exit;
+                        }
+                    }
+
+                    if ($this->_decoder->getElementStartTag(SYNC_MIMETRUNCATION)) {
+                        $collection['mimetruncation'] = $this->_decoder->getElementContent();
+                        if (!$this->_decoder->getElementEndTag()) {
+                            $this->_statusCode = self::STATUS_PROTERROR;
+                            $this->_handleError($collection);
+                            exit;
+                        }
+                    }
+
+                    if ($this->_decoder->getElementStartTag(SYNC_CONFLICT)) {
+                        $collection['conflict'] = $this->_decoder->getElementContent();
+                        if (!$this->_decoder->getElementEndTag()) {
+                            $this->_statusCode = self::STATUS_PROTERROR;
+                            $this->_handleError;
+                            exit;
+                        }
+                    }
+                    $e = $this->_decoder->peek();
+                    if ($e[Horde_ActiveSync_Wbxml::EN_TYPE] == Horde_ActiveSync_Wbxml::EN_TYPE_ENDTAG) {
+                        $this->_decoder->getElementEndTag();
+                        break;
+                    }
+                }
+            }
+
+            if ($this->_statusCode == self::STATUS_SUCCESS) {
+                /* Initialize the state */
+                $state = &$this->_driver->getStateObject($collection);
+                try {
+                    $state->loadState($collection['synckey']);
+                } catch (Horde_ActiveSync_Exception $e) {
+                    $this->_statusCode = self::STATUS_KEYMISM;
+                    $this->_handleError($collection);
+                    exit;
+                }
+
+                /* compatibility mode - get folderid from the state directory */
+                if (!isset($collection['id'])) {
+                    $collection['id'] = $state->getFolderData($this->_devId, $collection['class']);
+                }
+
+                /* compatibility mode - set default conflict behavior if no
+                 * conflict resolution algorithm is set */
+                if (!isset($collection['conflict'])) {
+                    $collection['conflict'] = SYNC_CONFLICT_OVERWRITE_PIM;
+                }
+            }
+
+            if ($this->_decoder->getElementStartTag(SYNC_COMMANDS)) {
+                /* Configure importer with last state */
+                $importer = $this->_driver->getImporter();
+                $importer->init($state, $collection['id'], $collection['synckey'], $collection['conflict']);
+                $nchanges = 0;
+                while (1) {
+                    // MODIFY or REMOVE or ADD or FETCH
+                    $element = $this->_decoder->getElement();
+                    if ($element[Horde_ActiveSync_Wbxml::EN_TYPE] != Horde_ActiveSync_Wbxml::EN_TYPE_STARTTAG) {
+                        $this->_decoder->_ungetElement($element);
+                        break;
+                    }
+
+                    $nchanges++;
+
+                    if ($this->_decoder->getElementStartTag(SYNC_SERVERENTRYID)) {
+                        $serverid = $this->_decoder->getElementContent();
+
+                        if (!$this->_decoder->getElementEndTag()) {// end serverid
+                            $this->_statusCode = self::STATUS_PROTERROR;
+                            $this->_handleError($collection);
+                            exit;
+                        }
+                    } else {
+                        $serverid = false;
+                    }
+
+                    if ($this->_decoder->getElementStartTag(SYNC_CLIENTENTRYID)) {
+                        $clientid = $this->_decoder->getElementContent();
+
+                        if (!$this->_decoder->getElementEndTag()) { // end clientid
+                            $this->_statusCode = self::STATUS_PROTERROR;
+                            $this->_handleError($collection);
+                            exit;
+                        }
+                    } else {
+                        $clientid = false;
+                    }
+
+                    /* Create Streamer object from messages passed from PIM */
+                    if ($this->_decoder->getElementStartTag(SYNC_DATA)) {
+                        switch ($collection['class']) {
+                        case 'Email':
+                            //@TODO
+                            //$appdata = new SyncMail();
+                            //$appdata->decode($decoder);
+                            // Remove error code when implemented.
+                            $this->_statusCode = self::STATUS_SERVERERROR;
+                            break;
+                        case 'Contacts':
+                            $appdata = new Horde_ActiveSync_Message_Contact(
+                                array('logger' => $this->_logger,
+                                      'protocolversion' => $this->_version));
+                            $appdata->decodeStream($this->_decoder);
+                            break;
+                        case 'Calendar':
+                            $appdata = new Horde_ActiveSync_Message_Appointment(array('logger' => $this->_logger));
+                            $appdata->decodeStream($this->_decoder);
+                            break;
+                        case 'Tasks':
+                            $appdata = new Horde_ActiveSync_Message_Task(array('logger' => $this->_logger));
+                            $appdata->decodeStream($this->_decoder);
+                            break;
+                        }
+                        if (!$this->_decoder->getElementEndTag()) {
+                            // End application data
+                            $this->_statusCode = self::STATUS_PROTERROR;
+                            break;
+                        }
+                    }
+
+                    switch ($element[Horde_ActiveSync_Wbxml::EN_TAG]) {
+                    case SYNC_MODIFY:
+                        if (isset($appdata)) {
+                            // Currently, 'read' is only sent by the PDA when it
+                            // is ONLY setting the read flag.
+                            if (isset($appdata->read)) {
+                                $importer->ImportMessageReadFlag($serverid, $appdata->read);
+                            } else {
+                                $importer->ImportMessageChange($serverid, $appdata);
+                            }
+                            $collection['importedchanges'] = true;
+                        }
+                        break;
+                    case SYNC_ADD:
+                        if (isset($appdata)) {
+                            $id = $importer->ImportMessageChange(false, $appdata);
+                            if ($clientid && $id) {
+                                $collection['clientids'][$clientid] = $id;
+                                $collection['importedchanges'] = true;
+                            }
+                        }
+                        break;
+                    case SYNC_REMOVE:
+                        if (isset($collection['deletesasmoves'])) {
+                            $folderid = $this->_driver->GetWasteBasket();
+
+                            if ($folderid) {
+                                $importer->ImportMessageMove($serverid, $folderid);
+                                $collection['importedchanges'] = true;
+                                break;
+                            }
+                        }
+
+                        $importer->ImportMessageDeletion($serverid);
+                        $collection['importedchanges'] = true;
+                        break;
+                    case SYNC_FETCH:
+                        array_push($collection['fetchids'], $serverid);
+                        break;
+                    }
+
+                    if (!$this->_decoder->getElementEndTag()) {
+                        // end change/delete/move
+                        $this->_statusCode = self::STATUS_PROTERROR;
+                        $this->_handleSyncError($collection);
+                        exit;
+                    }
+                }
+
+                $this->_logger->debug(sprintf('[Horde_ActiveSync::handleSync] Processed %d incoming changes', $nchanges));
+
+                if (!$this->_decoder->getElementEndTag()) {
+                    // end commands
+                    $this->_statusCode = self::STATUS_PROTERROR;
+                    $this->_handleError($collection);
+                    exit;
+                }
+            }
+
+            if (!$this->_decoder->getElementEndTag()) {
+                // end collection
+                $this->_statusCode = self::STATUS_PROTERROR;
+                $this->_handleError($collection);
+                exit;
+            }
+
+            array_push($collections, $collection);
+        }
+
+        if (!$this->_decoder->getElementEndTag()) {
+            // end collections
+            return false;
+        }
+
+        if (!$this->_decoder->getElementEndTag()) {
+            // end sync
+            return false;
+        }
+
+        /* Start output to PIM */
+        $this->_encoder->startWBXML();
+        $this->_encoder->startTag(SYNC_SYNCHRONIZE);
+        $this->_encoder->startTag(SYNC_FOLDERS);
+        foreach ($collections as $collection) {
+
+            /* Get new synckey if needed */
+            if (isset($collection['importedchanges']) ||
+                isset($collection['getchanges']) ||
+                $collection['synckey'] == "0") {
+
+                $collection['newsynckey'] = $state->getNewSyncKey($collection['synckey']);
+            }
+
+            $this->_encoder->startTag(SYNC_FOLDER);
+            $this->_encoder->startTag(SYNC_FOLDERTYPE);
+            $this->_encoder->content($collection['class']);
+            $this->_encoder->endTag();
+
+            $this->_encoder->startTag(SYNC_SYNCKEY);
+            if (isset($collection['newsynckey'])) {
+                $this->_encoder->content($collection['newsynckey']);
+            } else {
+                $this->_encoder->content($collection['synckey']);
+            }
+            $this->_encoder->endTag();
+
+            $this->_encoder->startTag(SYNC_FOLDERID);
+            $this->_encoder->content($collection['id']);
+            $this->_encoder->endTag();
+
+            $this->_encoder->startTag(SYNC_STATUS);
+            $this->_encoder->content($this->_statusCode);
+            $this->_encoder->endTag();
+
+            /* Check the mimesupport because we need it for advanced emails */
+            $mimesupport = isset($collection['mimesupport']) ? $collection['mimesupport'] : 0;
+
+            /* Output server IDs for new items we received and added from PIM */
+            if (isset($collection['clientids']) || count($collection['fetchids']) > 0) {
+                $this->_encoder->startTag(SYNC_REPLIES);
+                foreach ($collection["clientids"] as $clientid => $serverid) {
+                    $this->_encoder->startTag(SYNC_ADD);
+                    $this->_encoder->startTag(SYNC_CLIENTENTRYID);
+                    $this->_encoder->content($clientid);
+                    $this->_encoder->endTag();
+                    $this->_encoder->startTag(SYNC_SERVERENTRYID);
+                    $this->_encoder->content($serverid);
+                    $this->_encoder->endTag();
+                    $this->_encoder->startTag(SYNC_STATUS);
+                    $this->_encoder->content(1);
+                    $this->_encoder->endTag();
+                    $this->_encoder->endTag();
+                }
+
+                /* Output any FETCH requests */
+                foreach ($collection['fetchids'] as $id) {
+                    $data = $this->_driver->Fetch($collection['id'], $id, $mimesupport);
+                    if ($data !== false) {
+                        $this->_encoder->startTag(SYNC_FETCH);
+                        $this->_encoder->startTag(SYNC_SERVERENTRYID);
+                        $this->_encoder->content($id);
+                        $this->_encoder->endTag();
+                        $this->_encoder->startTag(SYNC_STATUS);
+                        $this->_encoder->content(1);
+                        $this->_encoder->endTag();
+                        $this->_encoder->startTag(SYNC_DATA);
+                        $data->encodeStream($this->_encoder);
+                        $this->_encoder->endTag();
+                        $this->_encoder->endTag();
+                    } else {
+                        $this->_logger->err(sprintf('[Horde_ActiveSync::handleSync] Unable to fetch %s', $id));
+                    }
+                }
+                $this->_encoder->endTag();
+            }
+
+            /* Send server changes to PIM */
+            if (isset($collection['getchanges'])) {
+                $filtertype = isset($collection['filtertype']) ? $collection['filtertype'] : false;
+                $streamer = new Horde_ActiveSync_Streamer($this->_encoder, $collection['class']);
+                $exporter = $this->_driver->getExporter();
+                $exporter->init($state, $streamer, $collection);
+                $changecount = $exporter->getChangeCount();
+                if (!empty($collection['windowsize']) && $changecount > $collection['windowsize']) {
+                    $this->_encoder->startTag(SYNC_MOREAVAILABLE, false, true);
+                }
+
+                /* Output message changes per folder */
+                $this->_encoder->startTag(SYNC_COMMANDS);
+
+                // Stream the changes to the PDA
+                $n = 0;
+                while (1) {
+                    $progress = $exporter->syncronize();
+                    if (!is_array($progress)) {
+                        break;
+                    }
+                    $n++;
+
+                    if (!empty($collection['windowsize']) && $n >= $collection['windowsize']) {
+                        $this->_logger->info(sprintf('[Horde_ActiveSync::handleSync] Exported maxItems of messages: %d - more available.',  $collection['windowsize']));
+                        break;
+                    }
+                }
+                $this->_encoder->endTag();
+            }
+
+            $this->_encoder->endTag();
+
+            /* Save the sync state for the next time */
+            if (isset($collection['newsynckey'])) {
+                if (!empty($exporter) || !empty($importer) || !empty($streamer) || $collection['synckey'] == 0)  {
+                    $state->setNewSyncKey($collection['newsynckey']);
+                    $state->save();
+                } else {
+                    $this->_logger->err(sprintf('[Horde_ActiveSync::handleSync] Error saving %s - no state information available.', $collection["newsynckey"]));
+                }
+            }
+        }
+
+        $this->_encoder->endTag();
+
+        $this->_encoder->endTag();
+
+        return true;
+    }
+
+    /**
+     * Helper for handling sync errors
+     *
+     * @param <type> $collection
+     */
+    private function _handleError($collection)
+    {
+        $this->_encoder->startWBXML();
+        $this->_encoder->startTag(SYNC_SYNCHRONIZE);
+
+        $this->_encoder->startTag(SYNC_FOLDERS);
+
+        /* Get new synckey if needed */
+        if ($this->_statusCode == self::STATUS_KEYMISM ||
+            isset($collection['importedchanges']) ||
+            isset($collection['getchanges']) ||
+            $collection['synckey'] == "0") {
+
+            $collection['newsynckey'] = Horde_ActiveSync_State_Base::getNewSyncKey(($this->_statusCode == self::STATUS_KEYMISM) ? 0 : $collection['synckey']);
+            // @TODO: Need to reset the state??
+        }
+
+        $this->_encoder->startTag(SYNC_FOLDER);
+
+        $this->_encoder->startTag(SYNC_FOLDERTYPE);
+        $this->_encoder->content($collection['class']);
+        $this->_encoder->endTag();
+
+        $this->_encoder->startTag(SYNC_SYNCKEY);
+        if (isset($collection['newsynckey'])) {
+            $this->_encoder->content($collection['newsynckey']);
+        } else {
+            $this->_encoder->content($collection['synckey']);
+        }
+        $this->_encoder->endTag();
+
+        $this->_encoder->startTag(SYNC_FOLDERID);
+        $this->_encoder->content($collection['id']);
+        $this->_encoder->endTag();
+
+        $this->_encoder->startTag(SYNC_STATUS);
+        $this->_encoder->content($this->_statusCode);
+        $this->_encoder->endTag();
+
+        $this->_encoder->endTag(); // SYNC_FOLDER
+        $this->_encoder->endTag();
+        $this->_encoder->endTag();
+    }
+
+}
diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/State/Base.php b/framework/ActiveSync/lib/Horde/ActiveSync/State/Base.php
new file mode 100644 (file)
index 0000000..77b3248
--- /dev/null
@@ -0,0 +1,291 @@
+<?php
+/**
+ * Base class for managing everything related to state:
+ *
+ *     Persistence of state data
+ *     Generating delta between server and PIM
+ *     Caching PING related state (hearbeat interval, folder list etc...)
+ *
+ * Copyright 2010 The Horde Project (http://www.horde.org)
+ *
+ * @author Michael J. Rubinsky <mrubinsk@horde.org>
+ * @package Horde_ActiveSync
+ */
+abstract class Horde_ActiveSync_State_Base
+{
+
+    /**
+     * Filtertype constants
+     */
+    const FILTERTYPE_ALL = 0;
+    const FILTERTYPE_1DAY = 1;
+    const FILTERTYPE_3DAYS = 2;
+    const FILTERTYPE_1WEEK = 3;
+    const FILTERTYPE_2WEEKS = 4;
+    const FILTERTYPE_1MONTH = 5;
+    const FILTERTYPE_3MONTHS = 6;
+    const FILTERTYPE_6MONTHS = 7;
+
+    /**
+     * Configuration parameters
+     *
+     * @var array
+     */
+    protected $_params;
+
+    /**
+     * Caches the current state(s) in memory
+     *
+     * @var array
+     */
+    protected $_stateCache;
+
+    /**
+     * The syncKey for the current request.
+     *
+     * @var string
+     */
+    protected $_syncKey;
+
+    /**
+     * The backend driver
+     *
+     * @param Horde_ActiveSync_Driver_Base
+     */
+    protected $_backend;
+
+    /**
+     * The collection array for the collection we are currently syncing.
+     * Keys include:
+     *   'class'      - The collection class Contacts, Calendar etc...
+     *   'synckey'    - The current synckey
+     *   'newsynckey' - The new synckey sent back to the PIM
+     *   'id'         - Server folder id
+     *   'filtertype' - Filter
+     *   'conflict'   - Conflicts
+     *   'truncation' - Truncation
+     *
+     *
+     * @var array
+     */
+    protected $_collection;
+
+    /**
+     * Logger instance
+     *
+     * @var Horde_Log_Logger
+     */
+    protected $_logger;
+
+    /**
+     * The PIM device id. Needed for PING requests
+     *
+     * @var string
+     */
+    protected $_devId;
+
+    /**
+     * Const'r
+     *
+     * @param array $collection  A collection array
+     * @param array $params  All configuration parameters, requirements.
+     *
+     * @return Horde_ActiveSync_State_Base
+     */
+    public function __construct($params = array())
+    {
+        $this->_params = $params;
+    }
+
+    /**
+     * Update the $oldKey syncState to $newKey.
+     *
+     * @param string $newKey
+     *
+     * @return void
+     */
+    public function setNewSyncKey($newKey)
+    {
+        $this->_syncKey = $newKey;
+    }
+
+    /**
+     * Loads the initial state from storage for the specified syncKey and
+     * intializes the stateMachine for use.
+     *
+     * @param string $key  The key for the syncState or pingState to load.
+     *
+     * @return array The state array
+     */
+    abstract public function loadState($syncKey);
+
+    /**
+     * Load the ping state for the given device id
+     *
+     * @param string $devid  The device id.
+     */
+    abstract public function loadPingCollectionState($devid);
+
+    /**
+     * Get the list of known folders for the specified syncState
+     *
+     * @param string $syncKey  The syncState key
+     *
+     * @return array  An array of server folder ids
+     */
+    abstract public function getKnownFolders();
+
+    /**
+     * Save the current syncstate to storage
+     *
+     * @param string $syncKey
+     */
+    abstract public function save();
+
+    /**
+     * Update the state for a specific syncKey
+     *
+     * @param <type> $type
+     * @param <type> $change
+     * @param <type> $key
+     */
+    abstract public function updateState($type, $change);
+
+    /**
+     * Obtain the diff between PIM and server
+     */
+    abstract public function getChanges();
+
+    /**
+     * Determines if the server version of the message represented by $stat
+     * conflicts with the PIM version of the message according to the current
+     * state.
+     *
+     * @param array $stat   A message stat array
+     * @param string $type  The type of change (change, delete, add)
+     *
+     * @return boolean
+     */
+    abstract public function isConflict($stat, $type);
+
+    /**
+     * Set the backend driver
+     * (should really only be called by a backend object when passing this
+     * object to client code)
+     *
+     * @param Horde_ActiveSync_Driver_Base $backend  The backend driver
+     *
+     * @return void
+     */
+    public function setBackend(Horde_ActiveSync_Driver_Base $backend)
+    {
+        $this->_backend = $backend;
+    }
+
+    /**
+     * Initialize the state object
+     *
+     * @param array $collection  The collection array
+     *
+     * @return void
+     */
+    public function init($collection = array())
+    {
+        $this->_collection = $collection;
+    }
+
+    /**
+     * Set the logger instance for this object.
+     *
+     * @param Horde_Log_Logger $logger
+     */
+    public function setLogger($logger)
+    {
+        $this->_logger = $logger;
+    }
+
+    /**
+     * Gets the new sync key for a specified sync key. You must save the new
+     * sync state under this sync key when done sync'ing by calling
+     * setNewSyncKey(), then save().
+     *
+     * @param string $syncKey  The old syncKey
+     *
+     * @return string  The new synckey
+     */
+    static public function getNewSyncKey($syncKey)
+    {
+        if (empty($syncKey)) {
+            return '{' . self::uuid() . '}' . '1';
+        } else {
+            if (preg_match('/^s{0,1}\{([a-fA-F0-9-]+)\}([0-9]+)$/', $syncKey, $matches)) {
+                $n = $matches[2];
+                $n++;
+
+                return '{' . $matches[1] . '}' . $n;
+            }
+
+            // @TODO: should this thrown an exception instead of returning false?
+            return false;
+        }
+    }
+
+    /**
+     * Generate a uid for the sync key
+     *
+     * @return unknown_type
+     */
+    static public function uuid()
+    {
+        return sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
+                    mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ),
+                    mt_rand( 0, 0x0fff ) | 0x4000,
+                    mt_rand( 0, 0x3fff ) | 0x8000,
+                    mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ));
+    }
+
+   /**
+    * Returns the timestamp of the earliest modification time to consider
+    *
+    * @param integer $restrict  The time period to restrict to
+    *
+    * @return integer
+    */
+    static protected function _getCutOffDate($restrict)
+    {
+        switch($restrict) {
+        case self::FILTERTYPE_1DAY:
+            $back = 60 * 60 * 24;
+            break;
+        case self::FILTERTYPE_3DAYS:
+            $back = 60 * 60 * 24 * 3;
+            break;
+        case self::FILTERTYPE_1WEEK:
+            $back = 60 * 60 * 24 * 7;
+            break;
+        case self::FILTERTYPE_2WEEKS:
+            $back = 60 * 60 * 24 * 14;
+            break;
+        case self::FILTERTYPE_1MONTH:
+            $back = 60 * 60 * 24 * 31;
+            break;
+        case self::FILTERTYPE_3MONTHS:
+            $back = 60 * 60 * 24 * 31 * 3;
+            break;
+        case self::FILTERTYPE_6MONTHS:
+            $back = 60 * 60 * 24 * 31 * 6;
+            break;
+        default:
+            break;
+        }
+
+        if (isset($back))
+        {
+            $date = time() - $back;
+            return $date;
+        } else {
+            return 0; // unlimited
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/State/File.php b/framework/ActiveSync/lib/Horde/ActiveSync/State/File.php
new file mode 100644 (file)
index 0000000..edfb9be
--- /dev/null
@@ -0,0 +1,623 @@
+<?php
+/**
+ * File based state management. Some code based on the Z-Push project's
+ * diff backend, original copyright notice appears below.
+ *
+ * Copyright 2010 The Horde Project (http://www.horde.org)
+ *
+ * @author Michael J. Rubinsky <mrubinsk@horde.org>
+ * @package Horde_ActiveSync
+ */
+/**
+ * File      :   statemachine.php
+ * Project   :   Z-Push
+ * Descr     :   This class handles state requests;
+ *               Each differential mechanism can
+ *               store its own state information,
+ *               which is stored through the
+ *               state machine. SyncKey's are
+ *               of the  form {UUID}N, in which
+ *               UUID is allocated during the
+ *               first sync, and N is incremented
+ *               for each request to 'getNewSyncKey'.
+ *               A sync state is simple an opaque
+ *               string value that can differ
+ *               for each backend used - normally
+ *               a list of items as the backend has
+ *               sent them to the PIM. The backend
+ *               can then use this backend
+ *               information to compute the increments
+ *               with current data.
+ *
+ *               Old sync states are not deleted
+ *               until a sync state is requested.
+ *               At that moment, the PIM is
+ *               apparently requesting an update
+ *               since sync key X, so any sync
+ *               states before X are already on
+ *               the PIM, and can therefore be
+ *               removed. This algorithm is
+ *                automatically enforced by the
+ *                StateMachine class.
+ *
+ *
+ * Created   :   01.10.2007
+ *
+ * ï¿½ Zarafa Deutschland GmbH, www.zarafaserver.de
+ * This file is distributed under GPL v2.
+ * Consult LICENSE file for details
+ */
+class Horde_ActiveSync_State_File extends Horde_ActiveSync_State_Base
+{
+    /**
+     * Directory to store state files
+     *
+     * @var stirng
+     */
+    private $_stateDir;
+
+    /**
+     * Cache for ping state
+     *
+     * @var array
+     */
+    private $_pingState;
+
+    /**
+     * Local cache for changes to *send* to PIM
+     * (Will remain null until getChanges() is called)
+     *
+     * @var
+     */
+    private $_changes;
+
+    /**
+     * Const'r
+     *
+     * @param array  $params   Must contain 'stateDir' entry
+     *
+     * @return Horde_ActiveSync_StateMachine_File
+     */
+    public function __construct($params = array())
+    {
+        parent::__construct($params);
+
+        if (empty($this->_params['stateDir'])) {
+            throw new InvalidArgumentException('Missing required "stateDir" parameter.');
+        }
+
+        $this->_stateDir = $this->_params['stateDir'];
+    }
+
+    /**
+     * Load the sync state
+     *
+     * @return void
+     * @throws Horde_ActiveSync_Exception
+     */
+    public function loadState($syncKey)
+    {
+        /* Prime the state cache for the first sync */
+        if (empty($syncKey)) {
+            $this->_stateCache = array();
+            return;
+        }
+
+        // Check if synckey is allowed
+        if (!preg_match('/^s{0,1}\{([0-9A-Za-z-]+)\}([0-9]+)$/', $syncKey, $matches)) {
+            throw new Horde_ActiveSync_Exception('Invalid sync key');
+        }
+
+        // Cleanup all older syncstates
+        $this->_gc($syncKey);
+
+        // Read current sync state
+        $filename = $this->_stateDir . '/' . $syncKey;
+        if (!file_exists($filename)) {
+            throw new Horde_ActiveSync_Exception('Sync state not found');
+        }
+        $this->_stateCache = unserialize(file_get_contents($filename));
+
+        $this->_syncKey = $syncKey;
+    }
+
+    /**
+     * Determines if the server version of the message represented by $stat
+     * conflicts with the PIM version of the message according to the current
+     * state.
+     *
+     * @see Horde_ActiveSync_State_Base::isConflict()
+     */
+    public function isConflict($stat, $type)
+    {
+        foreach ($this->_stateCache as $state) {
+            if ($state['id'] == $stat['id']) {
+                $oldstat = $state;
+                break;
+            }
+        }
+
+        // New message on server - can never conflict, but we shouldn't be
+        // here in this case anyway, right?
+        if (!isset($oldstat)) {
+            return false;
+        }
+
+        if ($state['mod'] != $oldstat['mod']) {
+            // Changed here
+            if ($type == 'delete' || $type == 'change') {
+                // changed here, but deleted there -> conflict,
+                // or changed here and changed there -> conflict
+                return true;
+            } else {
+                // changed here, and other remote changes (move or flags)
+                return false;
+            }
+        }
+    }
+
+    /**
+     * Save the current state to storage
+     *
+     * @param string $syncKey  The sync key to save
+     *
+     * @return boolean
+     */
+    public function save()
+    {
+        return file_put_contents($this->_stateDir . '/' . $this->_syncKey, !empty($this->_stateCache) ? serialize($this->_stateCache) : '');
+    }
+
+    /**
+     * Update the state to reflect changes
+     *
+     * @param string $type       The type of change (change, delete, flags)
+     * @param array $change      Array describing change
+     *
+     * @return void
+     */
+    public function updateState($type, $change)
+    {
+        if (empty($this->_stateCache)) {
+            $this->_stateCache = array();
+        }
+
+        // Change can be a change or an add
+        if ($type == 'change') {
+            for($i = 0; $i < count($this->_stateCache); $i++) {
+                if($this->_stateCache[$i]['id'] == $change['id']) {
+                    $this->_stateCache[$i] = $change;
+                    /* If we have a pingState, keep it in sync */
+                    if (!empty($this->_pingState['collections'])) {
+                        $this->_pingState['collections'][$this->_collection['class']]['state'] = $this->_stateCache;
+                    }
+                    return;
+                }
+            }
+            // Not found, add as new
+            $this->_stateCache[] = $change;
+        } else {
+            for ($i = 0; $i < count($this->_stateCache); $i++) {
+                // Search for the entry for this item
+                if ($this->_stateCache[$i]['id'] == $change['id']) {
+                    if ($type == 'flags') {
+                        // Update flags
+                        $this->_stateCache[$i]['flags'] = $change['flags'];
+                    } elseif ($type == 'delete') {
+                        // Delete item
+                        array_splice($this->_stateCache, $i, 1);
+                    }
+                    break;
+                }
+            }
+        }
+
+        /* If we have a pingState, keep it in sync */
+        if (!empty($this->_pingState['collections'])) {
+            $this->_pingState['collections'][$this->_collection['class']]['state'] = $this->_stateCache;
+        }
+    }
+
+    /**
+     * Save folder data for a specific device
+     *
+     * @param string $devId  The device Id
+     * @param array $folders The folder data
+     *
+     * @return boolean
+     * @throws Horde_ActiveSync_Exception
+     */
+    public function setFolderData($devId, $folders)
+    {
+        if (!is_array($folders) || empty ($folders)) {
+            return false;
+        }
+
+        $unique_folders = array ();
+        foreach ($folders as $folder) {
+            // don't save folder-ids for emails
+            if ($folder->type == SYNC_FOLDER_TYPE_INBOX) {
+                continue;
+            }
+
+            // no folder from that type or the default folder
+            if (!array_key_exists($folder->type, $unique_folders) || $folder->parentid == 0) {
+                $unique_folders[$folder->type] = $folder->serverid;
+            }
+        }
+
+        // Treo does initial sync for calendar and contacts too, so we need to fake
+        // these folders if they are not supported by the backend
+        if (!array_key_exists(SYNC_FOLDER_TYPE_APPOINTMENT, $unique_folders)) {
+            $unique_folders[SYNC_FOLDER_TYPE_APPOINTMENT] = SYNC_FOLDER_TYPE_DUMMY;
+        }
+        if (!array_key_exists(SYNC_FOLDER_TYPE_CONTACT, $unique_folders)) {
+            $unique_folders[SYNC_FOLDER_TYPE_CONTACT] = SYNC_FOLDER_TYPE_DUMMY;
+
+        }
+        if (!file_put_contents($this->_stateDir . '/compat-' . $devId, serialize($unique_folders))) {
+            $this->logError('_saveFolderData: Data could not be saved!');
+            throw new Horde_ActiveSync_Exception('Folder data could not be saved');
+        }
+    }
+
+    /**
+     * Get the folder data for a specific device
+     *
+     * @param string $devId  The device id
+     * @param string $class  The folder class to fetch (Calendar, Contacts etc.)
+     *
+     * @return mixed  Either an array of folder data || false
+     */
+    public function getFolderData($devId, $class)
+    {
+        $filename = $this->_stateDir . '/compat-' . $devId;
+        if (file_exists($filename)) {
+            $arr = unserialize(file_get_contents($filename));
+            if ($class == "Calendar") {
+                return $arr[SYNC_FOLDER_TYPE_APPOINTMENT];
+            }
+            if ($class == "Contacts") {
+                return $arr[SYNC_FOLDER_TYPE_CONTACT];
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Return an array of known folders.
+     *
+     * @return array
+     */
+    public function getKnownFolders()
+    {
+        if (!isset($this->_stateCache)) {
+            throw new Horde_ActiveSync_Exception('Sync state not loaded');
+        }
+        $folders = array();
+        foreach ($this->_stateCache as $folder) {
+            $folders[] = $folder['id'];
+        }
+        return $folders;
+    }
+
+    /**
+     * Perform any initialization needed to deal with pingStates
+     * For this driver, it loads the device's state file.
+     *
+     * @param string $devId  The device id of the PIM to load PING state for
+     *
+     * @return The $collection array
+     */
+    public function initPingState($devId)
+    {
+        $this->_devId = $devId;
+        $file = $this->_stateDir . '/' . $devId;
+        if (file_exists($file)) {
+            $this->_pingState = unserialize(file_get_contents($file));
+        } else {
+            $this->_pingState = array(
+                'lifetime' => 0,
+                'collections' => array());
+        }
+
+        return $this->_pingState['collections'];
+    }
+
+    /**
+     * Load a specific collection's ping state
+     *
+     * @param array $pingCollection  The collection array from the PIM request
+     *
+     * @return void
+     * @throws Horde_ActiveSync_Exception
+     */
+    public function loadPingCollectionState($pingCollection)
+    {
+        if (empty($this->_pingState)) {
+            throw new Horde_ActiveSync_Exception('PING state not initialized');
+        }
+
+        $haveState = false;
+
+        /* Load any existing state */
+        // @TODO: I'm almost positive we need to key these by 'id', not 'class'
+        // but this is what z-push did so...
+        if (!empty($this->_pingState['collections'][$pingCollection['class']])) {
+            $this->_collection = $this->_pingState['collections'][$pingCollection['class']];
+            $this->_collection['synckey'] = $this->_devId;
+            $this->_stateCache = $this->_collection['state'];
+            $haveState = true;
+        }
+
+        /* Initialize state for this collection */
+        if (!$haveState) {
+            $this->_logger->debug('Empty state for '. $pingCollection['class']);
+
+            /* Start with empty state cache */
+            //$this->_stateCache[$pingCollection['id']] = array();
+
+            /* Init members for the getChanges call */
+            $this->_syncKey = $this->_devId;
+            $this->_collection = $pingCollection;
+            $this->_collection['synckey'] = $this->_devId;
+            $this->_collection['state'] = array();
+
+            /* If we are here, then the pingstate was empty, prime it */
+            $this->_pingState['collections'][$this->_collection['class']] = $this->_collection;
+
+            /* Need to load _stateCache so getChanges has it */
+            $this->_stateCache = array();
+
+            $changes = $this->getChanges();
+            foreach ($changes as $change) {
+                switch ($change['type']) {
+                case 'change':
+                    $stat = $this->_backend->StatMessage($this->_collection['id'], $change['id']);
+                    if (!$message = $this->_backend->GetMessage($this->_collection['id'], $change['id'], 0)) {
+                        throw new Horde_ActiveSync_Exception('Message not found');
+                    }
+                    if ($stat && $message) {
+                        $this->updateState('change', $stat);
+                    }
+                    break;
+
+                default:
+                    throw new Horde_ActiveSync_Exception('Unexpected change type in loadPingState');
+                }
+            }
+
+            $this->_pingState['collections'][$this->_collection['class']]['state'] = $this->_stateCache;
+            $this->savePingState();
+        }
+    }
+
+    /**
+     * Save the current ping state to storage
+     *
+     * @param string $devId      The PIM device id
+     * @param integer $lifetime  The ping heartbeat/lifetime interval
+     *
+     * @return boolean
+     * @throws Horde_ActiveSync_Exception
+     */
+    public function savePingState()
+    {
+        if (empty($this->_pingState)) {
+            throw new Horde_ActiveSync_Exception('PING state not initialized');
+        }
+        $state = serialize(array('lifetime' => $this->_pingState['lifetime'], 'collections' => $this->_pingState['collections']));
+
+        return file_put_contents($this->_stateDir . '/' . $this->_devId, $state);
+    }
+
+    /**
+     * Return the heartbeat interval, or zero if we have no existing state
+     *
+     * @param string $devId
+     *
+     * @return integer  The hearbeat interval, or zero if not found.
+     * @throws Horde_ActiveSync_Exception
+     */
+    public function getPingLifetime()
+    {
+        if (empty($this->_pingState)) {
+            throw new Horde_ActiveSync_Exception('PING state not initialized');
+        }
+
+        return (!$this->_pingState) ? 0 : $this->_pingState['lifetime'];
+    }
+
+    public function setPingLifetime($lifetime)
+    {
+        $this->_pingState['lifetime'] = $lifetime;
+    }
+
+    /**
+     *
+     * @param <type> $collectionClass  The collection we are syncing
+     *
+     * @return <type>
+     */
+    public function getChanges($flags = 0)
+    {
+        $syncState = empty($this->_stateCache) ? array() : $this->_stateCache;
+        $cutoffdate = self::_getCutOffDate(!empty($this->_collection['filtertype']) ? $this->_collection['filtertype'] : 0);
+
+        if (!empty($this->_collection['id'])) {
+            $folderId = $this->_collection['id'];
+            $this->_logger->debug('Initializing message diff engine');
+            if (!$syncState) {
+                $syncState = array();
+            }
+            $this->_logger->debug(count($syncState) . ' messages in state');
+
+            //do nothing if it is a dummy folder
+            if ($folderId != SYNC_FOLDER_TYPE_DUMMY) {
+                // on ping: check if backend supports alternative PING mechanism & use it
+                if ($this->_collection['class'] === false && $flags == BACKEND_DISCARD_DATA && $this->_backend->AlterPing()) {
+                    //@TODO - look at the passing of syncstate here - should probably pass self??
+                    // Not even sure if we need this AlterPing?
+                    $this->_changes = $this->_backend->AlterPingChanges($folderId, $syncState);
+                } else {
+                    // Get our lists - syncstate (old)  and msglist (new)
+                    $msglist = $this->_backend->GetMessageList($this->_collection['id'], $cutoffdate);
+                    if ($msglist === false) {
+                        return false;
+                    }
+                    $this->_changes = $this->_getDiff($syncState, $msglist);
+                }
+            }
+            $this->_logger->debug('Found ' . count($this->_changes) . ' message changes');
+
+        } else {
+
+            $this->_logger->debug('Initializing folder diff engine');
+            $folderlist = $this->_backend->getFolderList();
+            if ($folderlist === false) {
+                return false;
+            }
+
+            $this->_changes = $this->_getDiff($syncState, $folderlist);
+            $this->_logger->debug('Config: Found ' . count($this->_changes) . ' folder changes');
+        }
+
+        return $this->_changes;
+    }
+
+    public function getChangeCount()
+    {
+        if (!isset($this->_changes)) {
+            $this->getChanges();
+            //throw new Horde_ActiveSync_Exception('Changes not yet retrieved. Must call getChanges() first');
+        }
+        return count($this->_changes);
+    }
+
+    /**
+     * Garbage collector - clean up from previous sync
+     * requests.
+     *
+     * @params string $syncKey  The sync key
+     *
+     * @throws Horde_ActiveSync_Exception
+     * @return boolean?
+     */
+    protected function _gc($syncKey)
+    {
+        if (!preg_match('/^s{0,1}\{([0-9A-Za-z-]+)\}([0-9]+)$/', $syncKey, $matches)) {
+            return false;
+        }
+        $guid = $matches[1];
+        $n = $matches[2];
+
+        $dir = opendir($this->_stateDir);
+        if (!$dir) {
+            return false;
+        }
+        while ($entry = readdir($dir)) {
+            if (preg_match('/^s{0,1}\{([0-9A-Za-z-]+)\}([0-9]+)$/', $entry, $matches)) {
+                if ($matches[1] == $guid && $matches[2] < $n) {
+                    unlink($this->_stateDir . '/' . $entry);
+                }
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     *
+     * @param $old
+     * @param $new
+     * @return unknown_type
+     */
+    private function _getDiff($old, $new)
+    {
+        $changes = array();
+
+        // Sort both arrays in the same way by ID
+        usort($old, array(__CLASS__, 'RowCmp'));
+        usort($new, array(__CLASS__, 'RowCmp'));
+
+        $inew = 0;
+        $iold = 0;
+
+        // Get changes by comparing our list of messages with
+        // our previous state
+        while (1) {
+            $change = array();
+
+            if ($iold >= count($old) || $inew >= count($new)) {
+                break;
+            }
+
+            if ($old[$iold]['id'] == $new[$inew]['id']) {
+                // Both messages are still available, compare flags and mod
+                if (isset($old[$iold]['flags']) && isset($new[$inew]['flags']) && $old[$iold]['flags'] != $new[$inew]['flags']) {
+                    // Flags changed
+                    $change['type'] = 'flags';
+                    $change['id'] = $new[$inew]['id'];
+                    $change['flags'] = $new[$inew]['flags'];
+                    $changes[] = $change;
+                }
+
+                if ($old[$iold]['mod'] != $new[$inew]['mod']) {
+                    $change['type'] = 'change';
+                    $change['id'] = $new[$inew]['id'];
+                    $changes[] = $change;
+                }
+
+                $inew++;
+                $iold++;
+            } else {
+                if ($old[$iold]['id'] > $new[$inew]['id']) {
+                    // Message in state seems to have disappeared (delete)
+                    $change['type'] = 'delete';
+                    $change['id'] = $old[$iold]['id'];
+                    $changes[] = $change;
+                    $iold++;
+                } else {
+                    // Message in new seems to be new (add)
+                    $change['type'] = 'change';
+                    $change['flags'] = SYNC_NEWMESSAGE;
+                    $change['id'] = $new[$inew]['id'];
+                    $changes[] = $change;
+                    $inew++;
+                }
+            }
+        }
+
+        while ($iold < count($old)) {
+            // All data left in _syncstate have been deleted
+            $change['type'] = 'delete';
+            $change['id'] = $old[$iold]['id'];
+            $changes[] = $change;
+            $iold++;
+        }
+
+        while ($inew < count($new)) {
+            // All data left in new have been added
+            $change['type'] = 'change';
+            $change['flags'] = SYNC_NEWMESSAGE;
+            $change['id'] = $new[$inew]['id'];
+            $changes[] = $change;
+            $inew++;
+        }
+
+        return $changes;
+    }
+
+    /**
+     *
+     * @param $a
+     * @param $b
+     * @return unknown_type
+     */
+    static public function RowCmp($a, $b)
+    {
+        return $a['id'] < $b['id'] ? 1 : -1;
+    }
+
+}
\ No newline at end of file
diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/State/History.php b/framework/ActiveSync/lib/Horde/ActiveSync/State/History.php
new file mode 100644 (file)
index 0000000..a50e8ab
--- /dev/null
@@ -0,0 +1,595 @@
+<?php
+/**
+ * Horde_History based state management.
+ *
+ * Copyright 2010 The Horde Project (http://www.horde.org)
+ *
+ * @author Michael J. Rubinsky <mrubinsk@horde.org>
+ * @package Horde_ActiveSync
+ */
+class Horde_ActiveSync_State_History extends Horde_ActiveSync_State_Base
+{
+    /**
+     * Cache for ping state
+     *
+     * @var array
+     */
+    private $_pingState;
+
+    /**
+     * The timestamp for the last syncKey
+     *
+     * @var timestamp
+     */
+    private $_lastSyncTS;
+
+    /**
+     * The current sync timestamp
+     *
+     * @var timestamp
+     */
+    private $_thisSyncTS;
+
+
+    /**
+     * Local cache of changes that need to be sent
+     *
+     * @var array
+     */
+    private $_changes;
+
+    /**
+     * DB handle
+     *
+     * @var Horde_Db_Adapter_Base
+     */
+    protected $_db;
+
+    /**
+     * Const'r
+     *
+     * @param array  $params   Must contain:
+     *      'db'  - Horde_Db
+     *      'syncStateTable' - Name of table for storing syncstate
+     *      'pingTable'      - Name of table for storing ping data
+     *      'syncChangesTable'  - Name of table for remembering what changes
+     *                            are due to PIM import so we don't mirror the
+     *                            changes back to the PIM on next Sync
+     *
+     * @return Horde_ActiveSync_StateMachine_File
+     */
+    public function __construct($params = array())
+    {
+        parent::__construct($params);
+
+        if (empty($this->_params['db']) || !($this->_params['db'] instanceof Horde_Db_Adapter_Base)) {
+            throw new InvalidArgumentException('Missing or invalid Horde_Db parameter.');
+        }
+        $this->_params = $params['db'];
+    }
+
+    /**
+     * Load the sync state
+     *
+     * @return void
+     * @throws Horde_ActiveSync_Exception
+     */
+    public function loadState($syncKey)
+    {
+        if (empty($syncKey)) {
+            return;
+        }
+
+        // Check if synckey is allowed
+        if (!preg_match('/^s{0,1}\{([0-9A-Za-z-]+)\}([0-9]+)$/', $syncKey, $matches)) {
+            throw new Horde_ActiveSync_Exception('Invalid sync key');
+        }
+        $this->_syncKey = $syncKey;
+
+        try {
+            $results = $this->_db->selectOne('SELECT sync_data, sync_devId, sync_time FROM ' . $this->_syncStateTable . ' WHERE sync_key = ?', array($this->_syncKey));
+        } catch (Horde_Db_Exception $e) {
+            throw new Horde_ActiveSync_Exception($e);
+        }
+
+        /* Load the previous syncState from storage */
+        $this->_lastSyncTS = $results['sync_time'];
+        $this->_devId = $results['sync_devId'];
+        $this->_changes = unserialize(sync_data);
+    }
+
+    /**
+     * Determines if the server version of the message represented by $stat
+     * conflicts with the PIM version of the message.  For this driver, this is
+     * true whenever $lastSyncTime is older then $stat['mod']. Method is only
+     * called from the Importer during an import of a non-new change from the
+     * PIM.
+     *
+     * @see Horde_ActiveSync_State_Base::isConflict()
+     */
+    public function isConflict($stat, $type)
+    {
+        // $stat == server's message information
+         if ($stat['mod'] > $this->_lastSyncTS) {
+             if ($type == 'delete' || $type == 'change') {
+                 // changed here - deleted there
+                 // changed here - changed there
+                 return true;
+             } else {
+                 // all other remote cahnges are fine (move/flags)
+                 return false;
+             }
+        }
+    }
+
+    /**
+     * Save the current state to storage
+     *
+     * @return boolean
+     * @throws Horde_ActiveSync_Exception
+     */
+    public function save()
+    {
+        // Update state table to remember this last synctime and key
+        $sql = 'INSERT INTO ' . $this->_syncStateTable . ' (sync_key, sync_data, sync_devId, sync_time) VALUES (?, ?, ?, ?)';
+
+        /* Remember any left over changes */
+        $data = (isset($this->_changes) ? serialize($this->_changes) : serialize(array()));
+
+        try {
+            $this->_db->insert($sql, array($this->_syncKey, $data, $this->_devId, $this->_thisSyncTS));
+        } catch (Horde_Db_Exception $e) {
+            throw new Horde_ActiveSync_Exception($e);
+        }
+        
+        return true;
+    }
+
+    /**
+     * Update the state to reflect changes
+     *
+     * Notes: Since PIM changes are dealt with before Server changes, we can
+     * use a null $_changes array to detect what we are updating for. If we
+     * are importing PIM changes, need to update the syncChangesTable so we
+     * don't mirror back the changes on next sync. If we are exporting server
+     * changes, we need to track which changes have been sent (by removing them
+     * from _changes) so we know which items to send on the next sync if a
+     * MOREAVAILBLE response was needed.
+     *
+     * @param string $type   The type of change (change, delete, flags)
+     * @param array $change  Array describing change
+     *
+     * @return void
+     */
+    public function updateState($type, $change)
+    {
+       if (!isset($this->_changes)) {
+           /* We must be updating state during receiving changes from PIM */
+           $sql = 'INSERT INTO ' . $this->_syncChangesTable . ' (message_uid, sync_mod_time, sync_key) VALUES (?, ?, ?)';
+           try {
+               $this->_db->insert($sql, array($change['id'], time(), $this->_syncKey));
+           } catch (Horde_Db_Exception $e) {
+               throw new Horde_ActiveSync_Exception($e);
+           }
+       } else {
+           /* When sending server changes, $this->_changes will contain all
+            * changes. Need to track which ones are sent since we might not
+            * send all of them.
+            */
+           for ($i = 0; $i < count($this->_changes); $i++) {
+               if ($this->_changes[$i]['id'] == $change['id']) {
+                   unset($this->_changes[$i]);
+               }
+           }
+       }
+    }
+
+    /**
+     * Save folder data for a specific device
+     *
+     * @param string $devId  The device Id
+     * @param array $folders The folder data
+     *
+     * @return boolean
+     * @throws Horde_ActiveSync_Exception
+     */
+    public function setFolderData($devId, $folders)
+    {
+        if (!is_array($folders) || empty ($folders)) {
+            return false;
+        }
+
+        $unique_folders = array ();
+        foreach ($folders as $folder) {
+            // don't save folder-ids for emails
+            if ($folder->type == SYNC_FOLDER_TYPE_INBOX) {
+                continue;
+            }
+
+            // no folder from that type or the default folder
+            if (!array_key_exists($folder->type, $unique_folders) || $folder->parentid == 0) {
+                $unique_folders[$folder->type] = $folder->serverid;
+            }
+        }
+
+        // Treo does initial sync for calendar and contacts too, so we need to fake
+        // these folders if they are not supported by the backend
+        if (!array_key_exists(SYNC_FOLDER_TYPE_APPOINTMENT, $unique_folders)) {
+            $unique_folders[SYNC_FOLDER_TYPE_APPOINTMENT] = SYNC_FOLDER_TYPE_DUMMY;
+        }
+        if (!array_key_exists(SYNC_FOLDER_TYPE_CONTACT, $unique_folders)) {
+            $unique_folders[SYNC_FOLDER_TYPE_CONTACT] = SYNC_FOLDER_TYPE_DUMMY;
+        }
+        /* Storage to SQL? */
+//
+//        if (!file_put_contents($this->_stateDir . '/compat-' . $devId, serialize($unique_folders))) {
+//            $this->logError('_saveFolderData: Data could not be saved!');
+//            throw new Horde_ActiveSync_Exception('Folder data could not be saved');
+//        }
+    }
+
+    /**
+     * Get the folder data for a specific device
+     *
+     * @param string $devId  The device id
+     * @param string $class  The folder class to fetch (Calendar, Contacts etc.)
+     *
+     * @return mixed  Either an array of folder data || false
+     */
+    public function getFolderData($devId, $class)
+    {
+//        $filename = $this->_stateDir . '/compat-' . $devId;
+//        if (file_exists($filename)) {
+//            $arr = unserialize(file_get_contents($filename));
+//            if ($class == "Calendar") {
+//                return $arr[SYNC_FOLDER_TYPE_APPOINTMENT];
+//            }
+//            if ($class == "Contacts") {
+//                return $arr[SYNC_FOLDER_TYPE_CONTACT];
+//            }
+//        }
+//
+//        return false;
+    }
+
+    public function getKnownFolders($syncKey)
+    {
+
+        $sql = 'SELECT state_data from ' . $this->_table . ' WHERE state_syncKey = ?';
+        //
+        //
+
+    }
+
+    public function setKnownFolders($syncKey, $folders)
+    {
+        $sql = 'INSERT INTO ' . $this->_table . '....';
+
+        // Need to GC the table, delete all but the *two* most recent synckeys
+        // for this devId. Need the latest one, but also the previous one in
+        // case the device did not correctly receive the response - it will
+        // continue to send the previous syncKey, so we need to remember the
+        // state.
+
+    }
+
+    /**
+     * Perform any initialization needed to deal with pingStates
+     * For this driver, it loads the device's state file.
+     *
+     * @param string $devId  The device id of the PIM to load PING state for
+     *
+     * @return The $collection array
+     */
+    public function initPingState($devId)
+    {
+        $this->_devId = $devId;
+
+        $sql = 'SELECT ping_state FROM ' . $this->_pingTable . ' WHERE ping_devid = ?';
+
+        $this->_pingState = unserialize($results);
+        // Try to get pingstate from SQL (need lifetime and last synctime)
+        //$this->_pingState = unserialize($sqlResults);
+
+        // If no existing state - initialize
+        //        $this->_pingState = array(
+        //            'lifetime' => 0,
+        //            'collections' => array());
+
+        return $this->_pingState['collections'];
+    }
+
+    /**
+     * Load a specific collection's ping state
+     *
+     * @param array $pingCollection  The collection array from the PIM request
+     *
+     * @return void
+     * @throws Horde_ActiveSync_Exception
+     */
+    public function loadCollectionPingState($pingCollection)
+    {
+        if (empty($this->_pingState)) {
+            throw new Horde_ActiveSync_Exception('PING state not initialized');
+        }
+
+        $haveState = false;
+
+        /* Load any existing state */
+        // @TODO: I'm almost positive we need to key these by 'id', not 'class'
+        // but this is what z-push did so...
+        if (!empty($this->_pingState['collections'][$pingCollection['class']])) {
+            $this->_collection = $this->_pingState['collections'][$pingCollection['class']];
+            $this->_collection['synckey'] = $this->_devId;
+            //$this->_stateCache = $this->_collection['state'];
+            $haveState = true;
+        }
+
+        /* Initialize state for this collection */
+        if (!$haveState) {
+            $this->_logger->debug('Empty state for '. $pingCollection['class']);
+
+            /* Start with empty state cache */
+            //$this->_stateCache[$pingCollection['id']] = array();
+
+            /* Init members for the getChanges call */
+            $this->_syncKey = $this->_devId;
+            $this->_collection = $pingCollection;
+            $this->_collection['synckey'] = $this->_devId;
+            $this->_collection['state'] = array();
+
+            /* If we are here, then the pingstate was empty, prime it */
+            $this->_pingState['collections'][$this->_collection['class']] = $this->_collection;
+
+            /* Need to load _stateCache so getChanges has it */
+            $this->_stateCache = array();
+
+            $changes = $this->getChanges();
+            foreach ($changes as $change) {
+                switch ($change['type']) {
+                case 'change':
+                    $stat = $this->_backend->StatMessage($this->_collection['id'], $change['id']);
+                    if (!$message = $this->_backend->GetMessage($this->_collection['id'], $change['id'], 0)) {
+                        throw new Horde_ActiveSync_Exception('Message not found');
+                    }
+                    if ($stat && $message) {
+                        $this->updateState('change', $stat);
+                    }
+                    break;
+
+                default:
+                    throw new Horde_ActiveSync_Exception('Unexpected change type in loadPingState');
+                }
+            }
+
+            $this->_pingState['collections'][$this->_collection['class']]['state'] = $this->_stateCache;
+            $this->savePingState();
+        }
+    }
+
+    /**
+     * Save the current ping state to storage
+     *
+     * @param string $devId      The PIM device id
+     * @param integer $lifetime  The ping heartbeat/lifetime interval
+     *
+     * @return boolean
+     * @throws Horde_ActiveSync_Exception
+     */
+    public function savePingState()
+    {
+        if (empty($this->_pingState)) {
+            throw new Horde_ActiveSync_Exception('PING state not initialized');
+        }
+        $state = serialize(array('lifetime' => $this->_pingState['lifetime'], 'collections' => $this->_pingState['collections']));
+
+        // Need to write to DB
+        return ;//file_put_contents($this->_stateDir . '/' . $this->_devId, $state);
+    }
+
+    /**
+     * Return the heartbeat interval, or zero if we have no existing state
+     *
+     * @param string $devId
+     *
+     * @return integer  The hearbeat interval, or zero if not found.
+     * @throws Horde_ActiveSync_Exception
+     */
+    public function getPingLifetime()
+    {
+        if (empty($this->_pingState)) {
+            throw new Horde_ActiveSync_Exception('PING state not initialized');
+        }
+
+        return (!$this->_pingState) ? 0 : $this->_pingState['lifetime'];
+    }
+
+    public function setPingLifetime($lifetime)
+    {
+        $this->_pingState['lifetime'] = $lifetime;
+    }
+
+    /**
+     * Get all items that have changed since the last sync time
+     *
+     * @param integer $flags
+     *
+     * @return array
+     */
+    public function getChanges($flags = 0)
+    {
+        $cutoffdate = self::_getCutOffDate(!empty($this->_collection['filtertype']) ? $this->_collection['filtertype'] : 0);
+
+        if (!empty($this->_collection['id'])) {
+            $folderId = $this->_collection['id'];
+            $this->_logger->debug('Initializing message diff engine');
+
+            //do nothing if it is a dummy folder
+            if ($folderId != SYNC_FOLDER_TYPE_DUMMY) {
+                /* First, need to see if we have exising changes left over
+                 * from a previous sync that resulted in a MORE_AVAILABLE */
+                if (!$empty($this->_changes)) {
+                    return $this->_changes;
+                }
+
+                /* No existing changes, poll the backend */
+                $this->_thisSyncTS = time();
+                $this->_changes = $this->_backend->getServerChanges($folderId, $this->_lastSyncTS, $this->_thisSyncTS);
+            }
+            $this->_logger->debug('Found ' . count($this->_changes) . ' message changes');
+
+        } else {
+
+            $this->_logger->debug('Initializing folder diff engine');
+            $this->_thisSyncTS = time();
+            $folderlist = $this->_backend->getFolderList();
+            if ($folderlist === false) {
+                return false;
+            }
+
+            if (!isset($syncState) || !$syncState) {
+                $syncState = array();
+            }
+
+            $this->_changes = $this->_getDiff($syncState, $folderlist);
+            $this->_logger->debug('Config: Found ' . count($this->_changes) . ' folder changes');
+        }
+
+        return $this->_changes;
+    }
+
+    public function getChangeCount()
+    {
+        if (!isset($this->_changes)) {
+            $this->getChanges();
+            //throw new Horde_ActiveSync_Exception('Changes not yet retrieved. Must call getChanges() first');
+        }
+        return count($this->_changes);
+    }
+
+    /**
+     * Garbage collector - clean up from previous sync
+     * requests.
+     *
+     * @params string $syncKey  The sync key
+     *
+     * @throws Horde_ActiveSync_Exception
+     * @return boolean?
+     */
+    protected function _gc($syncKey)
+    {
+        if (!preg_match('/^s{0,1}\{([0-9A-Za-z-]+)\}([0-9]+)$/', $syncKey, $matches)) {
+            return false;
+        }
+        $guid = $matches[1];
+        $n = $matches[2];
+
+        $dir = opendir($this->_stateDir);
+        if (!$dir) {
+            return false;
+        }
+        while ($entry = readdir($dir)) {
+            if (preg_match('/^s{0,1}\{([0-9A-Za-z-]+)\}([0-9]+)$/', $entry, $matches)) {
+                if ($matches[1] == $guid && $matches[2] < $n) {
+                    unlink($this->_stateDir . '/' . $entry);
+                }
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     *
+     * @param $old
+     * @param $new
+     * @return unknown_type
+     */
+    private function _getDiff($old, $new)
+    {
+        $changes = array();
+
+        // Sort both arrays in the same way by ID
+        usort($old, array(__CLASS__, 'RowCmp'));
+        usort($new, array(__CLASS__, 'RowCmp'));
+
+        $inew = 0;
+        $iold = 0;
+
+        // Get changes by comparing our list of messages with
+        // our previous state
+        while (1) {
+            $change = array();
+
+            if ($iold >= count($old) || $inew >= count($new)) {
+                break;
+            }
+
+            if ($old[$iold]['id'] == $new[$inew]['id']) {
+                // Both messages are still available, compare flags and mod
+                if (isset($old[$iold]['flags']) && isset($new[$inew]['flags']) && $old[$iold]['flags'] != $new[$inew]['flags']) {
+                    // Flags changed
+                    $change['type'] = 'flags';
+                    $change['id'] = $new[$inew]['id'];
+                    $change['flags'] = $new[$inew]['flags'];
+                    $changes[] = $change;
+                }
+
+                if ($old[$iold]['mod'] != $new[$inew]['mod']) {
+                    $change['type'] = 'change';
+                    $change['id'] = $new[$inew]['id'];
+                    $changes[] = $change;
+                }
+
+                $inew++;
+                $iold++;
+            } else {
+                if ($old[$iold]['id'] > $new[$inew]['id']) {
+                    // Message in state seems to have disappeared (delete)
+                    $change['type'] = 'delete';
+                    $change['id'] = $old[$iold]['id'];
+                    $changes[] = $change;
+                    $iold++;
+                } else {
+                    // Message in new seems to be new (add)
+                    $change['type'] = 'change';
+                    $change['flags'] = SYNC_NEWMESSAGE;
+                    $change['id'] = $new[$inew]['id'];
+                    $changes[] = $change;
+                    $inew++;
+                }
+            }
+        }
+
+        while ($iold < count($old)) {
+            // All data left in _syncstate have been deleted
+            $change['type'] = 'delete';
+            $change['id'] = $old[$iold]['id'];
+            $changes[] = $change;
+            $iold++;
+        }
+
+        while ($inew < count($new)) {
+            // All data left in new have been added
+            $change['type'] = 'change';
+            $change['flags'] = SYNC_NEWMESSAGE;
+            $change['id'] = $new[$inew]['id'];
+            $changes[] = $change;
+            $inew++;
+        }
+
+        return $changes;
+    }
+
+     /**
+     *
+     * @param $a
+     * @param $b
+     * @return unknown_type
+     */
+    static public function RowCmp($a, $b)
+    {
+        return $a['id'] < $b['id'] ? 1 : -1;
+    }
+
+}
\ No newline at end of file
diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Streamer.php b/framework/ActiveSync/lib/Horde/ActiveSync/Streamer.php
new file mode 100644 (file)
index 0000000..19f3cca
--- /dev/null
@@ -0,0 +1,124 @@
+<?php
+/**
+ * File      :   streamimporter.php
+ * Project   :   Z-Push
+ * Descr     :   Stream import classes
+ *
+ * Created   :   01.10.2007
+ *
+ * ï¿½ Zarafa Deutschland GmbH, www.zarafaserver.de
+ * This file is distributed under GPL v2.
+ * Consult LICENSE file for details
+ */
+
+/**
+ *
+ *
+ */
+class Horde_ActiveSync_Streamer
+{
+    protected $_encoder;
+    protected $_type;
+    protected $_seenObjects;
+
+    /**
+     * Const'r
+     *
+     * @param Horde_ActiveSync_Wbxml_Encoder $encoder
+     * @param string $class  The collection class
+     *
+     * @return Horde_ActiveSync_Streamer
+     */
+    public function __construct(&$encoder, $class)
+    {
+        $this->_encoder = &$encoder;
+        $this->_type = $class;
+        $this->_seenObjects = array();
+    }
+
+    /**
+     *
+     * @param $id
+     * @param $message
+     * @return unknown_type
+     */
+    public function messageChange($id, $message)
+    {
+        if ($message->getClass() != $this->_type) {
+            return true; // ignore other types
+        }
+
+        // prevent sending the same object twice in one request
+        if (in_array($id, $this->_seenObjects)) {
+               return true;
+        }
+
+        $this->_seenObjects[] = $id;
+        if ($message->flags === false || $message->flags === SYNC_NEWMESSAGE) {
+            $this->_encoder->startTag(SYNC_ADD);
+        } else {
+            $this->_encoder->startTag(SYNC_MODIFY);
+        }
+
+        $this->_encoder->startTag(SYNC_SERVERENTRYID);
+        $this->_encoder->content($id);
+        $this->_encoder->endTag();
+        $this->_encoder->startTag(SYNC_DATA);
+        $message->encodeStream($this->_encoder);
+        $this->_encoder->endTag();
+        $this->_encoder->endTag();
+
+        return true;
+    }
+
+    /**
+     *
+     * @param $id
+     * @return unknown_type
+     */
+    public function messageDeletion($id)
+    {
+        $this->_encoder->startTag(SYNC_REMOVE);
+        $this->_encoder->startTag(SYNC_SERVERENTRYID);
+        $this->_encoder->content($id);
+        $this->_encoder->endTag();
+        $this->_encoder->endTag();
+
+        return true;
+    }
+
+    /**
+     *
+     * @param $id
+     * @param $flags
+     * @return unknown_type
+     */
+    public function messageReadFlag($id, $flags)
+    {
+        if ($this->_type != "syncmail") {
+            return true;
+        }
+        $this->_encoder->startTag(SYNC_MODIFY);
+        $this->_encoder->startTag(SYNC_SERVERENTRYID);
+        $this->_encoder->content($id);
+        $this->_encoder->endTag();
+        $this->_encoder->startTag(SYNC_DATA);
+        $this->_encoder->startTag(SYNC_POOMMAIL_READ);
+        $this->_encoder->content($flags);
+        $this->_encoder->endTag();
+        $this->_encoder->endTag();
+        $this->_encoder->endTag();
+
+        return true;
+    }
+
+    /**
+     *
+     * @param $message
+     * @return unknown_type
+     */
+    function messageMove($message)
+    {
+        return true;
+    }
+}
\ No newline at end of file
diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Timezone.php b/framework/ActiveSync/lib/Horde/ActiveSync/Timezone.php
new file mode 100644 (file)
index 0000000..589dfba
--- /dev/null
@@ -0,0 +1,199 @@
+<?php
+/**
+ * Utility functions for dealing with Microsoft ActiveSync's Timezone format.
+ *
+ * Copyright 2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author   Michael J. Rubinsky <mrubinsk@horde.org>
+ *
+ * @category Horde
+ * @package  Horde_Rpc
+ */
+class Horde_ActiveSync_Timezone
+{
+
+    /**
+     * Convert a timezone from the ActiveSync base64 structure to a TZ offset
+     * hash.
+     *
+     * @param base64 encoded timezone structure defined by MS as:
+     *  <pre>
+     *      typedef struct TIME_ZONE_INFORMATION {
+     *        LONG Bias;
+     *        WCHAR StandardName[32];
+     *        SYSTEMTIME StandardDate;
+     *        LONG StandardBias;
+     *        WCHAR DaylightName[32];
+     *        SYSTEMTIME DaylightDate;
+     *        LONG DaylightBias;};
+     *  </pre>
+     *
+     *  With the SYSTEMTIME format being:
+     *  <pre>
+     * typedef struct _SYSTEMTIME {
+     *     WORD wYear;
+     *     WORD wMonth;
+     *     WORD wDayOfWeek;
+     *     WORD wDay;
+     *     WORD wHour;
+     *     WORD wMinute;
+     *     WORD wSecond;
+     *     WORD wMilliseconds;
+     *   } SYSTEMTIME, *PSYSTEMTIME;
+     *  </pre>
+     *
+     *  See: http://msdn.microsoft.com/en-us/library/ms724950%28VS.85%29.aspx
+     *  and: http://msdn.microsoft.com/en-us/library/ms725481%28VS.85%29.aspx
+     *
+     * @return array  Hash of offset information
+     */
+    static public function getOffsetsFromSyncTZ($data)
+    {
+        $tz = unpack('lbias/a64stdname/vstdyear/vstdmonth/vstdday/vstdweek/vstdhour/vstdminute/vstdsecond/vstdmillis/' .
+                     'lstdbias/a64dstname/vdstyear/vdstmonth/vdstday/vdstweek/vdsthour/vdstminute/vdstsecond/vdstmillis/' .
+                     'ldstbias', base64_decode($data));
+        $tz['timezone'] = $tz['bias'];
+        $tz['timezonedst'] = $tz['dstbias'];
+
+        return $tz;
+    }
+
+
+    /**
+     * Build an ActiveSync TZ blob given a TZ Offset hash.
+     *
+     * @param array $offsets  A TZ offset hash
+     *
+     * @return string  A base64_encoded ActiveSync Timezone structure suitable
+     *                 for transmitting via wbxml.
+     */
+    static public function getSyncTZFromOffsets($offsets)
+    {
+        $packed = pack('la64vvvvvvvvla64vvvvvvvvl',
+                $offsets['bias'], '', 0, $offsets['stdmonth'], $offsets['stdday'], $offsets['stdweek'], $offsets['stdhour'], $offsets['stdminute'], $offsets['stdsecond'], $offsets['stdmillis'],
+                $offsets['stdbias'], '', 0, $offsets['dstmonth'], $offsets['dstday'], $offsets['dstweek'], $offsets['dsthour'], $offsets['dstminute'], $offsets['dstsecond'], $offsets['dstmillis'],
+                $offsets['dstbias']);
+
+        return base64_encode($packed);
+    }
+
+    /**
+     * Create a offset hash suitable for use in ActiveSync transactions
+     *
+     * @param Horde_Date $date  A date object representing the date to base the
+     *                          the tz data on.
+     */
+    static public function getOffsetsFromDate($date)
+    {
+        $offsets = array(
+               'bias' => 0,
+               'stdname' => '',
+               'stdyear' => 0,
+               'stdmonth' => 0,
+               'stdday' => 0,
+               'stdweek' => 0,
+               'stdhour' => 0,
+               'stdminute' => 0,
+               'stdsecond' => 0,
+               'stdmillis' => 0,
+               'stdbias' => 0,
+               'dstname' => '',
+               'dstyear' => 0,
+               'dstmonth' => 0,
+               'dstday' => 0,
+               'dstweek' => 0,
+               'dsthour' => 0,
+               'dstminute' => 0,
+               'dstsecond' => 0,
+               'dstmillis' => 0,
+               'dstbias' => 0
+        );
+
+        $timezone = $date->toDateTime()->getTimezone();
+        list($std, $dst) = self::_getTransitions($timezone, $date);
+        if ($std) {
+            $offsets['bias'] = $std['offset'] / 60 * -1;
+            if ($dst) {
+                $offsets = self::_generateOffsetsForTransition($offsets, $std, 'std');
+                $offsets = self::_generateOffsetsForTransition($offsets, $dst, 'dst');
+                $offsets['stdhour'] += $dst['offset'] / 3600;
+                $offsets['dsthour'] += $std['offset'] / 3600;
+                $offsets['dstbias'] = ($dst['offset'] - $std['offset']) / 60 * -1;
+            }
+        }
+
+        return $offsets;
+    }
+
+    /**
+     * Get the transition data for moving from DST to STD time.
+     *
+     * @param DateTimeZone $timezone  The timezone to get the transition for
+     * @param Horde_Date $date        The date to start from. Really only the
+     *                                year we are interested in is needed.
+     *
+     * @return array containing the the STD and DST transitions
+     */
+    static protected function _getTransitions($timezone, $date)
+    {
+        $std = $dst = null;
+        // @TODO PHP 5.3 lets you specify a start and end timestamp, probably
+        // should version sniff here for the improved performance. Just need
+        // to remember that the first transition structure will then be for
+        // the start date, so we should go back one year from $date, then ignore
+        // the first entry.
+        $transitions = $timezone->getTransitions();
+        foreach ($transitions as $i => $transition) {
+            $d = new Horde_Date($transition['time'], 'UTC');
+            if ($d->format('Y') == $date->format('Y')) {
+                if (isset($transitions[$i + 1])) {
+                    $next = new Horde_Date($transitions[$i + 1]['ts']);
+                    if ($d->format('Y') == $next->format('Y')) {
+                        $dst = $transition['isdst'] ? $transition : $transitions[$i + 1];
+                        $std = $transition['isdst'] ? $transitions[$i + 1] : $transition;
+                    } else {
+                        $dst = $transition['isdst'] ? $transition: null;
+                        $std = $transition['isdst'] ? null : $transition;
+                    }
+                }
+                break;
+            } elseif ($i == count($transitions) - 1) {
+                $std = $transition;
+            }
+        }
+
+        return array($std, $dst);
+    }
+
+    /**
+        * Calculate the offsets for the specified transition
+        *
+        * @param array $offsets      A TZ offset hash
+        * @param array $transition   A transition hash
+        * @param string $type        Transition type - dst or std
+     *
+        * @return array  A populated offset hash
+        */
+       static protected function _generateOffsetsForTransition($offsets, $transition, $type)
+       {
+           $transitionDate = new Horde_Date($transition['time'], 'UTC');
+        $offsets[$type . 'month'] = $transitionDate->format('n');
+        $offsets[$type . 'day'] = $transitionDate->format('w');
+        $offsets[$type . 'minute'] = (int)$transitionDate->format('i');
+        $offsets[$type . 'hour'] = $transitionDate->format('H');
+        for ($i = 5; $i > 0; $i--) {
+            $nth = clone($transitionDate);
+            $nth->setNthWeekday($transitionDate->format('w'), $i);
+            if ($transitionDate->compareDate($nth) == 0) {
+                $offsets[$type . 'week'] = $i;
+                break;
+            };
+        }
+
+        return $offsets;
+       }
+
+}
diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Wbxml.php b/framework/ActiveSync/lib/Horde/ActiveSync/Wbxml.php
new file mode 100644 (file)
index 0000000..c4de20d
--- /dev/null
@@ -0,0 +1,56 @@
+<?php
+/**
+ * ActiveSync specific WBXML handling. This (and all related code) needs to be
+ * refactored to use XML_WBXML, or the H4 equivelant when it is written...
+ *
+ * @author Michael J. Rubinsky <mrubinsk@horde.org>
+ * @package Horde_ActiveSync
+ */
+
+/**
+ * File      :   wbxml.php
+ * Project   :   Z-Push
+ * Descr     :   WBXML mapping file
+ *
+ * Created   :   01.10.2007
+ *
+ * ï¿½ Zarafa Deutschland GmbH, www.zarafaserver.de
+ * This file is distributed under GPL v2.
+ * Consult LICENSE file for details
+ */
+class Horde_ActiveSync_Wbxml
+{
+    // @TODO - debug should be a config parameter
+    const DEBUG = false;
+    const SWITCH_PAGE =     0x00;
+    const END =             0x01;
+    const ENTITY =          0x02;
+    const STR_I =           0x03;
+    const LITERAL =         0x04;
+    const EXT_I_0 =         0x40;
+    const EXT_I_1 =         0x41;
+    const EXT_I_2 =         0x42;
+    const PI =              0x43;
+    const LITERAL_C =       0x44;
+    const EXT_T_0 =         0x80;
+    const EXT_T_1 =         0x81;
+    const EXT_T_2 =         0x82;
+    const STR_T =           0x83;
+    const LITERAL_A =       0x84;
+    const EXT_0 =           0xC0;
+    const EXT_1 =           0xC1;
+    const EXT_2 =           0xC2;
+    const OPAQUE =          0xC3;
+    const LITERAL_AC =      0xC4;
+
+    const EN_TYPE =                1;
+    const EN_TAG =                 2;
+    const EN_CONTENT =             3;
+    const EN_FLAGS =               4;
+    const EN_ATTRIBUTES =          5;
+    const EN_TYPE_STARTTAG =       1;
+    const EN_TYPE_ENDTAG =         2;
+    const EN_TYPE_CONTENT =        3;
+    const EN_FLAGS_CONTENT =       1;
+    const EN_FLAGS_ATTRIBUTES =    2;
+}
\ No newline at end of file
diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Wbxml/Decoder.php b/framework/ActiveSync/lib/Horde/ActiveSync/Wbxml/Decoder.php
new file mode 100644 (file)
index 0000000..d638244
--- /dev/null
@@ -0,0 +1,590 @@
+<?php
+/**
+ * ActiveSync specific WBXML handling. This (and all related code) needs to be
+ * refactored to use XML_WBXML, or the H4 equivelant when it is written...
+ *
+ * @author Michael J. Rubinsky <mrubinsk@horde.org>
+ * @package Horde_ActiveSync
+ */
+
+/**
+ * File      :   wbxml.php
+ * Project   :   Z-Push
+ * Descr     :   WBXML mapping file
+ *
+ * Created   :   01.10.2007
+ *
+ * ï¿½ Zarafa Deutschland GmbH, www.zarafaserver.de
+ * This file is distributed under GPL v2.
+ * Consult LICENSE file for details
+ */
+class Horde_ActiveSync_Wbxml_Decoder
+{
+    // @TODO
+    /**
+     * The DTD
+     *
+     * @var array
+     */
+    protected $_dtd;
+
+    /**
+     * PHP input stream
+     *
+     * @var stream
+     */
+    private $_in;
+
+    // These seem to only be used in the Const'r, and I can't find any
+    // client code that access these properties...
+    public $version;
+    public $publicid;
+    public $publicstringid;
+    public $charsetid;
+
+    public $stringtable;
+
+    private $_tagcp = 0;
+    private $_attrcp = 0;
+    private $_ungetbuffer;
+    private $_logStack = array();
+
+    /**
+     * @var Horde_Log_Logger
+     */
+    private $_logger;
+
+    /**
+     * Const'r
+     *
+     * @param stream $input
+     * @param array $dtd
+     * @param array $config
+     *
+     * @return Horde_ActiveSync_Wbxml_Decoder
+     */
+    public function __construct($input, $dtd)
+    {
+        $this->_in = $input;
+        $this->_dtd = $dtd;
+        $this->_logger = new Horde_Support_Stub();
+        // @TODO - these don't seem to be used anywhere, do we really need
+        // to keep them in an instance variable?
+        $this->version = $this->_getByte();
+        $this->publicid = $this->_getMBUInt();
+        if ($this->publicid == 0) {
+            $this->publicstringid = $this->_getMBUInt();
+        }
+        $this->charsetid = $this->_getMBUInt();
+
+        // This is used, but not sure what the missing getStringTableEntry()
+        // method was supposed to do yet.
+        $this->stringtable = $this->_getStringTable();
+    }
+
+    public function setLogger(Horde_Log_Logger $logger)
+    {
+        $this->_logger = $logger;
+    }
+    
+    /**
+     * Returns either start, content or end, and auto-concatenates successive content
+     */
+    public function getElement()
+    {
+        $element = $this->getToken();
+
+        switch ($element[Horde_ActiveSync_Wbxml::EN_TYPE]) {
+        case Horde_ActiveSync_Wbxml::EN_TYPE_STARTTAG:
+            return $element;
+        case Horde_ActiveSync_Wbxml::EN_TYPE_ENDTAG:
+            return $element;
+        case Horde_ActiveSync_Wbxml::EN_TYPE_CONTENT:
+            while (1) {
+                $next = $this->getToken();
+                if ($next == false) {
+                    return false;
+                } elseif ($next[Horde_ActiveSync_Wbxml::EN_TYPE] == Horde_ActiveSync_Wbxml::EN_CONTENT) {
+                    $element[Horde_ActiveSync_Wbxml::EN_CONTENT] .= $next[Horde_ActiveSync_Wbxml::EN_CONTENT];
+                } else {
+                    $this->_ungetElement($next);
+                    break;
+                }
+            }
+            return $element;
+        }
+
+        return false;
+    }
+
+    /**
+     *
+     * @return unknown_type
+     */
+    public function peek()
+    {
+        $element = $this->getElement();
+        $this->_ungetElement($element);
+
+        return $element;
+    }
+
+    /**
+     *
+     * @param $tag
+     * @return unknown_type
+     */
+    public function getElementStartTag($tag)
+    {
+        $element = $this->getToken();
+
+        if ($element[Horde_ActiveSync_Wbxml::EN_TYPE] == Horde_ActiveSync_Wbxml::EN_TYPE_STARTTAG &&
+            $element[Horde_ActiveSync_Wbxml::EN_TAG] == $tag) {
+
+            return $element;
+        } else {
+            $this->_logger->debug('Unmatched tag' .  $tag . ':');
+            $this->_ungetElement($element);
+        }
+
+        return false;
+    }
+
+    /**
+     *
+     * @return unknown_type
+     */
+    public function getElementEndTag()
+    {
+        $element = $this->getToken();
+        if ($element[Horde_ActiveSync_Wbxml::EN_TYPE] == Horde_ActiveSync_Wbxml::EN_TYPE_ENDTAG) {
+            return $element;
+        } else {
+            $this->_logger->err('Unmatched end tag:');
+            $this->_logger->err(print_r($element, true));
+            //$bt = debug_backtrace();
+           // $c = count($bt);
+           // $this->_logger->err('From ' . $bt[$c-2]['file'] . ':' . $bt[$c - 2]['line']);
+            $this->_ungetElement($element);
+        }
+
+        return false;
+    }
+
+    /**
+     *
+     * @return unknown_type
+     */
+    public function getElementContent()
+    {
+        $element = $this->getToken();
+        if ($element[Horde_ActiveSync_Wbxml::EN_TYPE] == Horde_ActiveSync_Wbxml::EN_TYPE_CONTENT) {
+            return $element[Horde_ActiveSync_Wbxml::EN_CONTENT];
+        } else {
+            $this->_logger->err('Unmatched content:');
+            $this->_logger->err(print_r($element, true));
+            $this->_ungetElement($element);
+        }
+
+        return false;
+    }
+
+    /**
+     * @TODO: Do we need?
+     * @return unknown_type
+     */
+    public function getToken()
+    {
+        // See if there's something in the ungetBuffer
+        if ($this->_ungetbuffer) {
+            $element = $this->_ungetbuffer;
+            $this->_ungetbuffer = false;
+            return $element;
+        }
+
+        $el = $this->_getToken();
+        $this->_logToken($el);
+
+        return $el;
+    }
+
+    /**
+     *
+     * @param unknown_type $el
+     * @return unknown_type
+     */
+    private function _logToken($el)
+    {
+        $spaces = str_repeat(' ', count($this->_logStack));
+        switch ($el[Horde_ActiveSync_Wbxml::EN_TYPE]) {
+        case Horde_ActiveSync_Wbxml::EN_TYPE_STARTTAG:
+            if ($el[Horde_ActiveSync_Wbxml::EN_FLAGS] & Horde_ActiveSync_Wbxml::EN_FLAGS_CONTENT) {
+                $this->_logger->debug('I ' . $spaces . ' <' . $el[Horde_ActiveSync_Wbxml::EN_TAG] . '>');
+                array_push($this->_logStack, $el[Horde_ActiveSync_Wbxml::EN_TAG]);
+            } else {
+                $this->_logger->debug('I ' . $spaces . ' <' . $el[Horde_ActiveSync_Wbxml::EN_TAG] . '/>');
+            }
+            break;
+        case Horde_ActiveSync_Wbxml::EN_TYPE_ENDTAG:
+            $tag = array_pop($this->_logStack);
+            $this->_logger->debug('I ' . $spaces . '</' . $tag . '>');
+            break;
+        case Horde_ActiveSync_Wbxml::EN_TYPE_CONTENT:
+            $this->_logger->debug('I ' . $spaces . ' ' . $el[Horde_ActiveSync_Wbxml::EN_CONTENT]);
+            break;
+        }
+    }
+
+    /**
+     * Returns either a start tag, content or end tag
+     */
+   private function _getToken() {
+
+        // Get the data from the input stream
+        $element = array();
+
+        while (1) {
+            $byte = $this->_getByte();
+
+            if (!isset($byte)) {
+                break;
+            }
+
+            switch ($byte) {
+            case Horde_ActiveSync_Wbxml::SWITCH_PAGE:
+                $this->_tagcp = $this->_getByte();
+                continue;
+
+            case Horde_ActiveSync_Wbxml::END:
+                $element[Horde_ActiveSync_Wbxml::EN_TYPE] = Horde_ActiveSync_Wbxml::EN_TYPE_ENDTAG;
+                return $element;
+
+            case Horde_ActiveSync_Wbxml::ENTITY:
+                $entity = $this->_getMBUInt();
+                $element[Horde_ActiveSync_Wbxml::EN_TYPE] = Horde_ActiveSync_Wbxml::EN_TYPE_CONTENT;
+                $element[Horde_ActiveSync_Wbxml::EN_CONTENT] = $this->entityToCharset($entity);
+                return $element;
+
+            case Horde_ActiveSync_Wbxml::STR_I:
+                $element[Horde_ActiveSync_Wbxml::EN_TYPE] = Horde_ActiveSync_Wbxml::EN_TYPE_CONTENT;
+                $element[Horde_ActiveSync_Wbxml::EN_CONTENT] = $this->_getTermStr();
+                return $element;
+
+            case Horde_ActiveSync_Wbxml::LITERAL:
+                $element[Horde_ActiveSync_Wbxml::EN_TYPE] = Horde_ActiveSync_Wbxml::EN_TYPE_STARTTAG;
+                $element[Horde_ActiveSync_Wbxml::EN_TAG] = $this->_getStringTableEntry($this->_getMBUInt());
+                $element[Horde_ActiveSync_Wbxml::EN_FLAGS] = 0;
+                return $element;
+
+            case Horde_ActiveSync_Wbxml::EXT_I_0:
+            case Horde_ActiveSync_Wbxml::EXT_I_1:
+            case Horde_ActiveSync_Wbxml::EXT_I_2:
+                $this->_getTermStr();
+                // Ignore extensions
+                continue;
+
+            case Horde_ActiveSync_Wbxml::PI:
+                // Ignore PI
+                $this->_getAttributes();
+                continue;
+
+            case Horde_ActiveSync_Wbxml::LITERAL_C:
+                $element[Horde_ActiveSync_Wbxml::EN_TYPE] = Horde_ActiveSync_Wbxml::EN_TYPE_STARTTAG;
+                $element[Horde_ActiveSync_Wbxml::EN_TAG] = $this->_getStringTableEntry($this->_getMBUInt());
+                $element[Horde_ActiveSync_Wbxml::EN_FLAGS] = Horde_ActiveSync_Wbxml::EN_FLAGS_CONTENT;
+                return $element;
+
+            case Horde_ActiveSync_Wbxml::EXT_T_0:
+            case Horde_ActiveSync_Wbxml::EXT_T_1:
+            case Horde_ActiveSync_Wbxml::EXT_T_2:
+                $this->_getMBUInt();
+                // Ingore extensions;
+                continue;
+
+            case Horde_ActiveSync_Wbxml::STR_T:
+                $element[Horde_ActiveSync_Wbxml::EN_TYPE] = Horde_ActiveSync_Wbxml::EN_TYPE_CONTENT;
+                $element[Horde_ActiveSync_Wbxml::EN_CONTENT] = $this->_getStringTableEntry($this->_getMBUInt());
+                return $element;
+
+            case Horde_ActiveSync_Wbxml::LITERAL_A:
+                $element[Horde_ActiveSync_Wbxml::EN_TYPE] = Horde_ActiveSync_Wbxml::EN_TYPE_STARTTAG;
+                $element[Horde_ActiveSync_Wbxml::EN_TAG] = $this->_getStringTableEntry($this->_getMBUInt());
+                $element[Horde_ActiveSync_Wbxml::EN_ATTRIBUTES] = $this->_getAttributes();
+                $element[Horde_ActiveSync_Wbxml::EN_FLAGS] = Horde_ActiveSync_Wbxml::EN_FLAGS_ATTRIBUTES;
+                return $element;
+            case Horde_ActiveSync_Wbxml::EXT_0:
+            case Horde_ActiveSync_Wbxml::EXT_1:
+            case Horde_ActiveSync_Wbxml::EXT_2:
+                continue;
+
+            case Horde_ActiveSync_Wbxml::OPAQUE:
+                $length = $this->_getMBUInt();
+                $element[Horde_ActiveSync_Wbxml::EN_TYPE] = Horde_ActiveSync_Wbxml::EN_TYPE_CONTENT;
+                $element[Horde_ActiveSync_Wbxml::EN_CONTENT] = $this->_getOpaque($length);
+                return $element;
+
+            case Horde_ActiveSync_Wbxml::LITERAL_AC:
+                $element[Horde_ActiveSync_Wbxml::EN_TYPE] = Horde_ActiveSync_Wbxml::EN_TYPE_STARTTAG;
+                $element[Horde_ActiveSync_Wbxml::EN_TAG] = $this->_getStringTableEntry($this->_getMBUInt());
+                $element[Horde_ActiveSync_Wbxml::EN_ATTRIBUTES] = $this->_getAttributes();
+                $element[Horde_ActiveSync_Wbxml::EN_FLAGS] = Horde_ActiveSync_Wbxml::EN_FLAGS_ATTRIBUTES | Horde_ActiveSync_Wbxml::EN_FLAGS_CONTENT;
+                return $element;
+
+            default:
+                $element[Horde_ActiveSync_Wbxml::EN_TYPE] = Horde_ActiveSync_Wbxml::EN_TYPE_STARTTAG;
+                $element[Horde_ActiveSync_Wbxml::EN_TAG] = $this->_getMapping($this->_tagcp, $byte & 0x3f);
+                $element[Horde_ActiveSync_Wbxml::EN_FLAGS] = ($byte & 0x80 ? Horde_ActiveSync_Wbxml::EN_FLAGS_ATTRIBUTES : 0) | ($byte & 0x40 ? Horde_ActiveSync_Wbxml::EN_FLAGS_CONTENT : 0);
+                if ($byte & 0x80) {
+                    $element[Horde_ActiveSync_Wbxml::EN_ATTRIBUTES] = $this->_getAttributes();
+                }
+                return $element;
+            }
+        }
+    }
+
+    /**
+     *
+     * @param $element
+     * @return unknown_type
+     */
+    public function _ungetElement($element)
+    {
+        if ($this->_ungetbuffer) {
+            $this->_logger->err('Double unget!');
+        }
+        $this->_ungetbuffer = $element;
+    }
+
+    /**
+     *
+     * @return unknown_type
+     */
+    private function _getAttributes()
+    {
+        $attributes = array();
+        $attr = '';
+
+        while (1) {
+            $byte = $this->_getByte();
+            if (count($byte) == 0) {
+                break;
+            }
+
+            switch($byte) {
+                case Horde_ActiveSync_Wbxml::SWITCH_PAGE:
+                    $this->_attrcp = $this->_getByte();
+                    break;
+
+                case Horde_ActiveSync_Wbxml::END:
+                    if ($attr != '') {
+                        $attributes += $this->_splitAttribute($attr);
+                    }
+                    return $attributes;
+
+                case Horde_ActiveSync_Wbxml::ENTITY:
+                    $entity = $this->_getMBUInt();
+                    $attr .= $this->entityToCharset($entity);
+                    return $element;
+
+                case Horde_ActiveSync_Wbxml::STR_I:
+                    $attr .= $this->_getTermStr();
+                    return $element;
+
+                case Horde_ActiveSync_Wbxml::LITERAL:
+                    if ($attr != '') {
+                        $attributes += $this->_splitAttribute($attr);
+                    }
+                    $attr = $this->_getStringTableEntry($this->_getMBUInt());
+                    return $element;
+
+                case Horde_ActiveSync_Wbxml::EXT_I_0:
+                case Horde_ActiveSync_Wbxml::EXT_I_1:
+                case Horde_ActiveSync_Wbxml::EXT_I_2:
+                    $this->_getTermStr();
+                    continue;
+
+                case Horde_ActiveSync_Wbxml::PI:
+                case Horde_ActiveSync_Wbxml::LITERAL_C:
+                    // Invalid
+                    return false;
+
+                case Horde_ActiveSync_Wbxml::EXT_T_0:
+                case Horde_ActiveSync_Wbxml::EXT_T_1:
+                case Horde_ActiveSync_Wbxml::EXT_T_2:
+                    $this->_getMBUInt();
+                    continue;
+
+                case Horde_ActiveSync_Wbxml::STR_T:
+                    $attr .= $this->_getStringTableEntry($this->_getMBUInt());
+                    return $element;
+
+                case Horde_ActiveSync_Wbxml::LITERAL_A:
+                    return false;
+
+                case Horde_ActiveSync_Wbxml::EXT_0:
+                case Horde_ActiveSync_Wbxml::EXT_1:
+                case Horde_ActiveSync_Wbxml::EXT_2:
+                    continue;
+
+                case Horde_ActiveSync_Wbxml::OPAQUE:
+                    $length = $this->_getMBUInt();
+                    $attr .= $this->_getOpaque($length);
+                    return $element;
+
+                case Horde_ActiveSync_Wbxml::LITERAL_AC:
+                    return false;
+
+                default:
+                    if ($byte < 128) {
+                        if ($attr != '') {
+                            $attributes += $this->_splitAttribute($attr);
+                            $attr = '';
+                        }
+                    }
+
+                    $attr .= $this->_getMapping($this->_attrcp, $byte);
+                    break;
+            }
+        }
+
+    }
+
+    /**
+     *
+     * @param $attr
+     *
+     * @return unknown_type
+     */
+    private function _splitAttribute($attr)
+    {
+        $attributes = array();
+        $pos = strpos($attr,chr(61)); // equals sign
+        if ($pos) {
+            $attributes[substr($attr, 0, $pos)] = substr($attr, $pos+1);
+        } else {
+            $attributes[$attr] = null;
+        }
+
+        return $attributes;
+    }
+
+    /**
+     *
+     * @return unknown_type
+     */
+    private function _getTermStr()
+    {
+        $str = '';
+        while(1) {
+            $in = $this->_getByte();
+
+            if ($in == 0) {
+                break;
+            } else {
+                $str .= chr($in);
+            }
+        }
+
+        return $str;
+    }
+
+    /**
+     *
+     * @param $len
+     *
+     * @return unknown_type
+     */
+    private function _getOpaque($len)
+    {
+        return fread($this->_in, $len);
+    }
+
+    /**
+     *
+     * @return unknown_type
+     */
+    private function _getByte()
+    {
+        $ch = fread($this->_in, 1);
+        if (strlen($ch) > 0) {
+            $ch = ord($ch);
+            $this->_logger->debug('_getByte: ' . $ch);
+            return $ch;
+        } else {
+            return;
+        }
+    }
+
+    /**
+     *
+     * @return unknown_type
+     */
+    private function _getMBUInt()
+    {
+        $uint = 0;
+        while (1) {
+          $byte = $this->_getByte();
+          $uint |= $byte & 0x7f;
+          if ($byte & 0x80) {
+              $uint = $uint << 7;
+          } else {
+              break;
+          }
+        }
+
+        $this->_logger->debug('_getMBUInt(): ' . $uint);
+        return $uint;
+    }
+
+    /**
+     *
+     * @return unknown_type
+     */
+    private function _getStringTable()
+    {
+        $stringtable = '';
+        $length = $this->_getMBUInt();
+        if ($length > 0) {
+            $stringtable = fread($this->_in, $length);
+        }
+
+        $this->_logger->debug('_getStringTable(): ' . $stringtable);
+        return $stringtable;
+    }
+
+    /**
+     * Really don't know for sure what this method is supposed to do, it is
+     * called from numerous places in this class, but the original zpush code
+     * did not contain this method...so, either it's completely broken, or
+     * normal use-cases do not reach the calling code. Either way, it needs to
+     * eventually be fixed.
+     *
+     * @param unknown_type $id
+     *
+     * @return unknown_type
+     */
+    private function _getStringTableEntry($id)
+    {
+        throw new Horde_ActiveSync_Exception('Not implemented');
+    }
+
+    /**
+     *
+     * @param $cp
+     * @param $id
+     * @return unknown_type
+     */
+    private function _getMapping($cp, $id)
+    {
+        if (!isset($this->_dtd['codes'][$cp]) || !isset($this->_dtd['codes'][$cp][$id])) {
+            return false;
+        } else {
+            if (isset($this->_dtd['namespaces'][$cp])) {
+                return $this->_dtd['namespaces'][$cp] . ':' . $this->_dtd['codes'][$cp][$id];
+            } else {
+                return $this->_dtd['codes'][$cp][$id];
+            }
+        }
+    }
+
+}
\ No newline at end of file
diff --git a/framework/ActiveSync/lib/Horde/ActiveSync/Wbxml/Encoder.php b/framework/ActiveSync/lib/Horde/ActiveSync/Wbxml/Encoder.php
new file mode 100644 (file)
index 0000000..27de787
--- /dev/null
@@ -0,0 +1,352 @@
+<?php
+/**
+ * ActiveSync specific WBXML handling. This (and all related code) needs to be
+ * refactored to use XML_WBXML, or the H4 equivelant when it is written...
+ *
+ * @author Michael J. Rubinsky <mrubinsk@horde.org>
+ * @package Horde_ActiveSync
+ */
+
+/**
+ * File      :   wbxml.php
+ * Project   :   Z-Push
+ * Descr     :   WBXML mapping file
+ *
+ * Created   :   01.10.2007
+ *
+ * ï¿½ Zarafa Deutschland GmbH, www.zarafaserver.de
+ * This file is distributed under GPL v2.
+ * Consult LICENSE file for details
+ */
+class Horde_ActiveSync_Wbxml_Encoder
+{
+    private $_dtd;
+    private $_out;
+    private $_tagcp;
+    private $_attrcp;
+    private $_logStack = array();
+
+    // We use a delayed output mechanism in which we only output a tag when it actually has something
+    // in it. This can cause entire XML trees to disappear if they don't have output data in them; Ie
+    // calling 'startTag' 10 times, and then 'endTag' will cause 0 bytes of output apart from the header.
+    // Only when content() is called do we output the current stack of tags=
+    private $_stack = array();
+
+    /**
+     * Const'r
+     *
+     * @param stream $output
+     * @param array $dtd
+     * @param array $config
+     *
+     * @return Horde_ActiveSync_Wbxml_Encoder
+     */
+    function __construct($output, $dtd)
+    {
+        $this->_out = $output;
+        $this->_tagcp = 0;
+        $this->_attrcp = 0;
+
+        // reverse-map the DTD
+        foreach ($dtd['namespaces'] as $nsid => $nsname) {
+            $this->_dtd['namespaces'][$nsname] = $nsid;
+        }
+
+        foreach ($dtd['codes'] as $cp => $value) {
+            $this->_dtd['codes'][$cp] = array();
+            foreach ($dtd['codes'][$cp] as $tagid => $tagname) {
+                $this->_dtd['codes'][$cp][$tagname] = $tagid;
+            }
+        }
+    }
+
+    public function setLogger(Horde_Log_Logger $logger)
+    {
+        $this->_logger = $logger;
+    }
+    /**
+     *
+     * @return unknown_type
+     */
+    public function startWBXML()
+    {
+        header('Content-Type: application/vnd.ms-sync.wbxml');
+        $this->_outByte(0x03); // WBXML 1.3
+        $this->_outMBUInt(0x01); // Public ID 1
+        $this->_outMBUInt(106); // UTF-8
+        $this->_outMBUInt(0x00); // string table length (0)
+    }
+
+    /**
+     *
+     * @param $tag
+     * @param $attributes
+     * @param $nocontent
+     * @return void
+     */
+    public function startTag($tag, $attributes = false, $nocontent = false)
+    {
+        $stackelem = array();
+        if (!$nocontent) {
+            $stackelem['tag'] = $tag;
+            $stackelem['attributes'] = $attributes;
+            $stackelem['nocontent'] = $nocontent;
+            $stackelem['sent'] = false;
+
+            array_push($this->_stack, $stackelem);
+
+            // If 'nocontent' is specified, then apparently the user wants to force
+            // output of an empty tag, and we therefore output the stack here
+        } else {
+            $this->_outputStack();
+            $this->_startTag($tag, $attributes, $nocontent);
+        }
+    }
+
+    /**
+     *
+     * @return void
+     */
+    public function endTag()
+    {
+        $stackelem = array_pop($this->_stack);
+        // Only output end tags for items that have had a start tag sent
+        if ($stackelem['sent']) {
+            $this->_endTag();
+        }
+    }
+
+    /**
+     *
+     * @param $content
+     *
+     * @return void
+     */
+    public function content($content)
+    {
+        // We need to filter out any \0 chars because it's the string terminator in WBXML. We currently
+        // cannot send \0 characters within the XML content anywhere.
+        $content = str_replace('\0', '', $content);
+        if ('x' . $content == 'x') {
+            return;
+        }
+        $this->_outputStack();
+        $this->_content($content);
+    }
+
+    /**
+     * Output any tags on the stack that haven't been output yet
+     *
+     * @TODO: Not 100% sure this can be private
+     */
+    private function _outputStack()
+    {
+        for ($i=0; $i < count($this->_stack); $i++) {
+            if (!$this->_stack[$i]['sent']) {
+                $this->_startTag($this->_stack[$i]['tag'], $this->_stack[$i]['attributes'], $this->_stack[$i]['nocontent']);
+                $this->_stack[$i]['sent'] = true;
+            }
+        }
+    }
+
+    // Outputs an actual start tag
+    private function _startTag($tag, $attributes = false, $nocontent = false)
+    {
+        $this->_logStartTag($tag, $attributes, $nocontent);
+        $mapping = $this->_getMapping($tag);
+        if (!$mapping) {
+            return false;
+        }
+
+        if ($this->_tagcp != $mapping['cp']) {
+            $this->_outSwitchPage($mapping['cp']);
+            $this->_tagcp = $mapping['cp'];
+        }
+
+        $code = $mapping['code'];
+        if (isset($attributes) && is_array($attributes) && count($attributes) > 0) {
+            $code |= 0x80;
+        }
+
+        if (!isset($nocontent) || !$nocontent) {
+            $code |= 0x40;
+        }
+
+        $this->_outByte($code);
+        if ($code & 0x80) {
+            $this->_outAttributes($attributes);
+        }
+    }
+
+    /**
+     * Outputs data
+     *
+     * @param $content
+     *
+     * @return unknown_type
+     */
+    private function _content($content)
+    {
+        $this->_logContent($content);
+        $this->_outByte(Horde_ActiveSync_Wbxml::STR_I);
+        $this->_outTermStr($content);
+    }
+
+    // Outputs an actual end tag
+    function _endTag() {
+        $this->_logEndTag();
+        $this->_outByte(Horde_ActiveSync_Wbxml::END);
+    }
+
+    /**
+     *
+     * @param $byte
+     * @return unknown_type
+     */
+    private function _outByte($byte)
+    {
+        fwrite($this->_out, chr($byte));
+    }
+
+    /**
+     *
+     * @param $uint
+     * @return unknown_type
+     */
+    private function _outMBUInt($uint)
+    {
+        while (1) {
+            $byte = $uint & 0x7f;
+            $uint = $uint >> 7;
+            if ($uint == 0) {
+                $this->_outByte($byte);
+                break;
+            } else {
+                $this->_outByte($byte | 0x80);
+            }
+        }
+    }
+
+    /**
+     *
+     * @param $content
+     * @return unknown_type
+     */
+    private function _outTermStr($content)
+    {
+        fwrite($this->_out, $content);
+        fwrite($this->_out, chr(0));
+    }
+
+    /**
+     *
+     * @return unknown_type
+     */
+    private function _outAttributes()
+    {
+        // We don't actually support this, because to do so, we would have
+        // to build a string table before sending the data (but we can't
+        // because we're streaming), so we'll just send an END, which just
+        // terminates the attribute list with 0 attributes.
+        $this->_outByte(Horde_ActiveSync_Wbxml::END);
+    }
+
+    /**
+     *
+     * @param $page
+     * @return unknown_type
+     */
+    private function _outSwitchPage($page)
+    {
+        $this->_outByte(Horde_ActiveSync_Wbxml::SWITCH_PAGE);
+        $this->_outByte($page);
+    }
+
+    /**
+     *
+     * @param $tag
+     * @return unknown_type
+     */
+    private function _getMapping($tag)
+    {
+        $mapping = array();
+        $split = $this->_splitTag($tag);
+        if (isset($split['ns'])) {
+            $cp = $this->_dtd['namespaces'][$split['ns']];
+        } else {
+            $cp = 0;
+        }
+
+        $code = $this->_dtd['codes'][$cp][$split['tag']];
+        $mapping['cp'] = $cp;
+        $mapping['code'] = $code;
+
+        return $mapping;
+    }
+
+    /**
+     *
+     * @param $fulltag
+     * @return unknown_type
+     */
+    private function _splitTag($fulltag)
+    {
+        $ns = false;
+        $pos = strpos($fulltag, chr(58)); // chr(58) == ':'
+        if ($pos) {
+            $ns = substr($fulltag, 0, $pos);
+            $tag = substr($fulltag, $pos+1);
+        } else {
+            $tag = $fulltag;
+        }
+
+        $ret = array();
+        if ($ns) {
+            $ret['ns'] = $ns;
+        }
+        $ret['tag'] = $tag;
+
+        return $ret;
+    }
+
+    /**
+     *
+     * @param $tag
+     * @param $attr
+     * @param $nocontent
+     * @return unknown_type
+     */
+    private function _logStartTag($tag, $attr, $nocontent)
+    {
+        $spaces = str_repeat(' ', count($this->_logStack));
+        if ($nocontent) {
+            $this->_logger->debug(sprintf('O %s <%s/>', $spaces, $tag));
+        } else {
+            array_push($this->_logStack, $tag);
+            $this->_logger->debug(sprintf('O %s <%s>', $spaces, $tag));
+        }
+    }
+
+    /**
+     *
+     * @return unknown_type
+     */
+    private function _logEndTag()
+    {
+        $spaces = str_repeat(' ', count($this->_logStack));
+        $tag = array_pop($this->_logStack);
+        $this->_logger->debug(sprintf('O %s <%s/>', $spaces, $tag));
+    }
+
+    /**
+     *
+     * @param unknown_type $content
+     * @return unknown_type
+     */
+    private function _logContent($content)
+    {
+        $spaces = str_repeat(' ', count($this->_logStack));
+        $this->_logger->debug('O ' . $spaces . $content);
+    }
+
+}
\ No newline at end of file
diff --git a/framework/ActiveSync/package.xml b/framework/ActiveSync/package.xml
new file mode 100644 (file)
index 0000000..dc09043
--- /dev/null
@@ -0,0 +1,131 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<package packagerversion="1.4.9" version="2.0" xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0
+http://pear.php.net/dtd/tasks-1.0.xsd
+http://pear.php.net/dtd/package-2.0
+http://pear.php.net/dtd/package-2.0.xsd">
+ <name>ActiveSync</name>
+ <channel>pear.horde.org</channel>
+ <summary>Horde ActiveSync Server Library</summary>
+ <description>This package provides libraries for implementing an ActiveSync server.</description>
+ <lead>
+  <name>Michael J. Rubinsky</name>
+  <user>mrubinsk</user>
+  <email>mrubinsk@horde.org</email>
+  <active>yes</active>
+ </lead>
+ <date>2010-01-10</date>
+ <version>
+  <release>0.1.0</release>
+  <api>0.1.0</api>
+ </version>
+ <stability>
+  <release>alpha</release>
+  <api>alpha</api>
+ </stability>
+ <license uri="http://www.gnu.org/licenses/gpl-2.0.html">GPLv2</license>
+ <notes>
+* Initial release
+ </notes>
+ <contents>
+  <dir name="/">
+   <dir name="lib">
+    <dir name="Horde">
+      <dir name="ActiveSync">
+       <dir name="State">
+         <file name="Base.php" role="php" />
+         <file name="File.php" role="php" />
+       </dir>
+       <dir name="Driver">
+         <dir name="Horde">
+           <dir name="Connector">
+             <file name="Registry.php" role="php" />
+           </dir>
+         </dir>
+         <file name="Base.php" role="php" />
+         <file name="Horde.php" role="php" />
+       </dir> <!-- /lib/Horde/ActiveSync/Driver -->
+       <dir name="Message">
+         <file name="Base.php" role="php" />
+         <file name="Appointment.php" role="php" />
+         <file name="Attendee.php" role="php" />
+         <file name="Contact.php" role="php" />
+         <file name="Folder.php" role="php" />
+         <file name="Recurrence.php" role="php" />
+         <file name="Exception.php" role="php" />
+       </dir> <!-- /lib/Horde/ActiveSync/Message -->
+       <dir name="Wbxml">
+         <file name="Decoder.php" role="php" />
+         <file name="Encoder.php" role="php" />
+       </dir> <!-- /lib/Horde/ActiveSync/Wbxml -->
+       <dir name="Request">
+         <file name="Base.php" role="php" />
+         <file name="FolderSync.php" role="php" />
+         <file name="Sync.php" role="php" />
+         <file name="Ping.php" role="php" />
+         <file name="Options.php" role="php" />
+         <file name="Provision.php" role="php" />
+       </dir>
+       <file name="Exception.php" role="php" />
+       <file name="ContentsCache.php" role="php" />
+       <file name="HierarchyCache.php" role="php" />
+       <file name="ImportContentsChangesStream.php" role="php" />
+       <file name="Wbxml.php" role="php" />
+       <file name="Timezone.php" role="php" />
+       <file name="Exporter.php" role="php" />
+       <file name="Importer.php" role="php" />
+       <file name="Streamer.php" role="php" />
+      </dir> <!-- /lib/Horde/ActiveSync -->
+      <file name="ActiveSync.php" role="php" />
+    </dir> <!-- /lib/Horde -->
+   </dir> <!-- /lib -->
+  </dir> <!-- / -->
+ </contents>
+ <dependencies>
+  <required>
+   <php>
+    <min>5.2.0</min>
+   </php>
+   <pearinstaller>
+    <min>1.5.0</min>
+   </pearinstaller>
+   <package>
+    <name>Date</name>
+    <channel>pear.horde.org</channel>
+   </package>
+  </required>
+ </dependencies>
+ <phprelease>
+  <filelist>
+   <install name="lib/Horde/ActiveSync/State/Base.php" as="Horde/ActiveSync/State/Base.php" />
+   <install name="lib/Horde/ActiveSync/State/File.php" as="Horde/ActiveSync/State/File.php" />
+   <install name="lib/Horde/ActiveSync/Driver/Horde/Connector/Registry.php" as="Horde/ActiveSync/Driver/Horde/Connector/Registry.php" />
+   <install name="lib/Horde/ActiveSync/Driver/Base.php" as="Horde/ActiveSync/Driver/Base.php" />
+   <install name="lib/Horde/ActiveSync/Driver/Horde.php" as="Horde/ActiveSync/Driver/Horde.php" />
+   <install name="lib/Horde/ActiveSync/Message/Base.php" as="Horde/ActiveSync/Message/Base.php" />
+   <install name="lib/Horde/ActiveSync/Message/Appointment.php" as="Horde/ActiveSync/Message/Appointment.php" />
+   <install name="lib/Horde/ActiveSync/Message/Attendee.php" as="Horde/ActiveSync/Message/Attendee.php" />
+   <install name="lib/Horde/ActiveSync/Message/Contact.php" as="Horde/ActiveSync/Message/Contact.php" />
+   <install name="lib/Horde/ActiveSync/Message/Folder.php" as="Horde/ActiveSync/Message/Folder.php" />
+   <install name="lib/Horde/ActiveSync/Message/Recurrence.php" as="Horde/ActiveSync/Message/Recurrence.php" />
+   <install name="lib/Horde/ActiveSync/Message/Exception.php" as="Horde/ActiveSync/Message/Exception.php" />
+   <install name="lib/Horde/ActiveSync/Wbxml/Decoder.php" as="Horde/ActiveSync/Wbxml/Decoder.php" />
+   <install name="lib/Horde/ActiveSync/Wbxml/Encoder.php" as="Horde/ActiveSync/Wbxml/Encoder.php" />
+   <install name="lib/Horde/ActiveSync/Request/Base.php" as="Horde/ActiveSync/Request/Base.php" />
+   <install name="lib/Horde/ActiveSync/Request/FolderSync.php" as="Horde/ActiveSync/Request/FolderSync.php" />
+   <install name="lib/Horde/ActiveSync/Request/Sync.php" as="Horde/ActiveSync/Request/Sync.php" />
+   <install name="lib/Horde/ActiveSync/Request/Ping.php" as="Horde/ActiveSync/Request/Ping.php" />
+   <install name="lib/Horde/ActiveSync/Request/Options.php" as="Horde/ActiveSync/Request/Options.php" />
+   <install name="lib/Horde/ActiveSync/Request/Provision.php" as="Horde/ActiveSync/Request/Provision.php" />
+   <install name="lib/Horde/ActiveSync/ContentsCache.php" as="Horde/ActiveSync/ContentsCache.php" />
+   <install name="lib/Horde/ActiveSync/Exception.php" as="Horde/ActiveSync/Exception.php" />
+   <install name="lib/Horde/ActiveSync/HierarchyCache.php" as="Horde/ActiveSync/HierarchyCache.php" />
+   <install name="lib/Horde/ActiveSync/ImportContentsChangesStream.php" as="Horde/ActiveSync/ImportContentsChangesStream.php" />
+   <install name="lib/Horde/ActiveSync/Wbxml.php" as="Horde/ActiveSync/Wbxml.php" />
+   <install name="lib/Horde/ActiveSync/Timezone.php" as="Horde/ActiveSync/Timezone.php" />
+   <install name="lib/Horde/ActiveSync/Importer.php" as="Horde/ActiveSync/Importer.php" />
+   <install name="lib/Horde/ActiveSync/Exporter.php" as="Horde/ActiveSync/Exporter.php" />
+   <install name="lib/Horde/ActiveSync/Streamer.php" as="Horde/ActiveSync/Streamer.php" />
+   <install name="lib/Horde/ActiveSync.php" as="Horde/ActiveSync.php" />
+  </filelist>
+ </phprelease>
+</package>
diff --git a/framework/ActiveSync/test/Horde/ActiveSync/AllTests.php b/framework/ActiveSync/test/Horde/ActiveSync/AllTests.php
new file mode 100644 (file)
index 0000000..9e7ff48
--- /dev/null
@@ -0,0 +1,35 @@
+<?php
+/**
+ * All tests for the Horde_ActiveSync:: package.
+ *
+ * PHP version 5
+ *
+ * @category Horde
+ * @package  ActiveSync
+ * @author   Michael J. Rubinsky <mrubinsk@horde.org>
+ */
+
+/**
+ * Define the main method
+ */
+if (!defined('PHPUnit_MAIN_METHOD')) {
+    define('PHPUnit_MAIN_METHOD', 'Horde_ActiveSync_AllTests::main');
+}
+
+/**
+ * Prepare the test setup.
+ */
+require_once 'Horde/Test/AllTests.php';
+
+/**
+ * @package    Horde_ActiveSync
+ * @subpackage UnitTests
+ */
+class Horde_ActiveSync_AllTests extends Horde_Test_AllTests
+{
+
+}
+
+if (PHPUnit_MAIN_METHOD == 'Horde_ActiveSync_AllTests::main') {
+    Horde_ActiveSync_AllTests::main('Horde_ActiveSync', __FILE__);
+}
diff --git a/framework/ActiveSync/test/Horde/ActiveSync/FileStateTest.php b/framework/ActiveSync/test/Horde/ActiveSync/FileStateTest.php
new file mode 100644 (file)
index 0000000..ebc6c2c
--- /dev/null
@@ -0,0 +1,136 @@
+<?php
+/* 
+ * Unit tests for the file state machine
+ *
+ * @author Michael J. Rubinsky <mrubinsk@horde.org>
+ * @category Horde
+ * @package Horde_ActiveSync
+ */
+require_once dirname(__FILE__) . '/fixtures/MockConnector.php';
+
+//FIXME: This can be removed once all the constants are class-constants
+require_once dirname(__FILE__) . '/../../../lib/Horde/ActiveSync.php';
+class Horde_ActiveSync_FileStateTest extends Horde_Test_Case
+{
+    /**
+     * Tests initial state loading from synckey zero.
+     * Should initialize the initial server state.
+     */
+    public function testCollectionSyncState()
+    {
+        $state = new Horde_ActiveSync_State_File(array('stateDir' => './'));
+        $state->init(array('id' => 'Contacts',
+                           'class' => 'Contacts'));
+
+        $state->loadState(0);
+
+        $contact = array(
+            '__key' => '9b07c14b086932e69cc7eb1baed0cc87',
+            '__owner' => 'mike',
+            '__type' => 'Object',
+            '__members' => '',
+            '__uid' => '20070112030611.62g1lg5nry80@test.theupstairsroom.com',
+            'firstname' => 'Michael',
+            'lastname' => 'Rubinsky',
+            'middlenames' => 'Joseph',
+            'namePrefix' => 'Dr',
+            'nameSuffix' => 'PharmD',
+            'name' => 'Michael Joseph Rubinsky',
+            'alias' => 'Me',
+            'birthday' => '1970-03-20',
+            'homeStreet' => '123 Main St.',
+            'homePOBox' => '',
+            'homeCity' => 'Anywhere',
+            'homeProvince' => 'NJ',
+            'homePostalCode' => '08080',
+            'homeCountry' => 'US',
+            'workStreet' => 'Kings Hwy',
+            'workPOBox' => '',
+            'workCity' => 'Somewhere',
+            'workProvince' => 'NJ',
+            'workPostalCode' => '08052',
+            'workCountry' => 'US',
+            'timezone' => 'America/New_York',
+            'email' => 'mrubinsk@horde.org',
+            'homePhone' => '(856)555-1234',
+            'workPhone' => '(856)555-5678',
+            'cellPhone' => '(609)555-9876',
+            'fax' => '',
+            'pager' => '',
+            'title' => '',
+            'role' => '',
+            'company' => '',
+            'category' => '',
+            'notes' => '',
+            'website' => '',
+            'freebusyUrl' => '',
+            'pgpPublicKey' => '',
+            'smimePublicKey' => '',
+        );
+
+        /* Create a mock driver with desired return values */
+        $fixture = array('contacts_list' => array('20070112030611.62g1lg5nry80@test.theupstairsroom.com'),
+                         'contacts_getActionTimestamp' => 0,
+                         'contacts_export' => $contact);
+        
+        $connector = new Horde_ActiveSync_MockConnector(array('fixture' => $fixture));
+        $driver = new Horde_ActiveSync_Driver_Horde(array('connector' => $connector,
+                                                          'state_basic' => $state));
+        $state->setBackend($driver);
+        $state->setLogger(new Horde_Support_Stub());
+        
+        /* Get the current state from the "server" */
+        $changes = $state->getChanges();
+        $this->assertEquals(1, $state->getChangeCount());
+        $this->assertEquals(array('type' => 'change', 'flags' => 'NewMessage', 'id' => '20070112030611.62g1lg5nry80@test.theupstairsroom.com'), $changes[0]);
+        
+        /* Import the state into the state object */
+        foreach($changes as $change) {
+            // We know it's always a 'change' since the above test passed
+            $stat = $driver->StatMessage('Contacts', $change['id'], 0);
+            $state->updateState('change', $stat, 0);
+        }
+
+        /* Get and set the new synckey */
+        $key = $state->getNewSyncKey(0);
+        $state->setNewSyncKey($key);
+        $state->save();
+
+        /* Check that the state was saved to file */
+        $this->assertFileExists($key);
+
+        /* ...and check that it contains the serialized state data
+         *  by reading it into a new object and performing another diff */
+        $newstate = new Horde_ActiveSync_State_File(array('stateDir' => './'));
+        $newstate->init(array('id' => 'Contacts',
+                           'class' => 'Contacts'));
+        $newstate->setBackend($driver);
+        $newstate->setLogger(new Horde_Support_Stub());
+        $newstate->loadState($key);
+        $this->assertEquals(0, $newstate->getChangeCount());
+
+        /* Clean up */
+        unlink($key);
+    }
+
+    public function testFolderSyncState()
+    {
+        // TODO
+    }
+    
+    public function testConflicts()
+    {
+        // TODO
+    }
+
+    public function testPingState()
+    {
+        // TODO
+    }
+
+    public function testPingStateUpdatedAfterStateUpdate()
+    {
+        
+    }
+}
+
diff --git a/framework/ActiveSync/test/Horde/ActiveSync/HordeDriverTest.php b/framework/ActiveSync/test/Horde/ActiveSync/HordeDriverTest.php
new file mode 100644 (file)
index 0000000..46011df
--- /dev/null
@@ -0,0 +1,270 @@
+<?php
+/* 
+ * Unit tests for the horde backend
+ *
+ * @author Michael J. Rubinsky <mrubinsk@horde.org>
+ * @category Horde
+ * @package Horde_ActiveSync
+ */
+require_once dirname(__FILE__) . '/fixtures/MockConnector.php';
+class Horde_ActiveSync_HordeDriverTest extends Horde_Test_Case
+{
+    /**
+     *
+     */
+    public function testConnectorRequiresRegistry()
+    {
+        $this->setExpectedException('Horde_ActiveSync_Exception');
+        $connector = new Horde_ActiveSync_Driver_Horde_Connector_Registry();
+
+        $registry = $this->getMockSkipConstructor('Horde_Registry');
+        $connector = new Horde_ActiveSync_Driver_Horde_Connector_Registry(array('registry' => $registry));
+        $this->assertType('Horde_ActiveSync_Driver_Horde_Connector_Registry', $connectory);
+    }
+
+    public function testDriverRequiresConnector()
+    {
+        $this->setExpectedException('Horde_ActiveSync_Exception');
+        $driver = new Horde_ActiveSync_Driver_Horde();
+
+        // Now for real
+        $registry = $this->getMockSkipConstructor('Horde_Registry');
+        $connector = new Horde_ActiveSync_Driver_Horde_Connector_Registry(array('registry' => $registry));
+        $state = $this->getMockSkipConstructor('Horde_ActiveSync_State_File');
+        $driver = new Horde_ActiveSync_Driver_Horde(array('connector' => $connector,
+                                                          'state_basic' => $state));
+        $this->assertType('Horde_ActiveSync_Driver', $driver);
+    }
+
+    /**
+     * Test that Horde_ActiveSync_Driver_Horde#getMessageList() returns expected
+     * data structures. Uses mock data via the MockConnector
+     * 
+     */
+    public function testGetMessageList()
+    {
+        // Test Contacts - simulates returning two contacts, both of which have no history modify entries.
+        $fixture = array('contacts_list' => array('20070112030603.249j42k3k068@test.theupstairsroom.com',
+                                                          '20070112030611.62g1lg5nry80@test.theupstairsroom.com'),
+                         'contacts_getActionTimestamp' => 0);
+
+        $connector = new Horde_ActiveSync_MockConnector(array('fixture' => $fixture));
+        $state = $this->getMockSkipConstructor('Horde_ActiveSync_State_File');
+        $driver = new Horde_ActiveSync_Driver_Horde(array('connector' => $connector,
+                                                          'state_basic' => $state));
+
+        $results = $driver->getMessageList('Contacts', time());
+
+        //$expected = array(
+        //    array('id' => '20070112030603.249j42k3k068@test.theupstairsroom.com',
+        //          'mod' => 0,
+        //          'flags' => 1),
+        //    array('id' => '20070112030611.62g1lg5nry80@test.theupstairsroom.com',
+        //          'mod' => 0,
+        //          'flags' => 1)
+        //);
+
+        $this->assertEquals(2, count($results));
+        foreach ($results as $result) {
+            if ($result['id'] != '20070112030603.249j42k3k068@test.theupstairsroom.com') {
+                $this->assertEquals('20070112030611.62g1lg5nry80@test.theupstairsroom.com', $result['id']);
+            } else {
+                $this->assertEquals('20070112030603.249j42k3k068@test.theupstairsroom.com', $result['id']);
+            }
+        }
+    }
+
+    /**
+     * Test retrieving a message from storage.
+     * Mocks the registry response.
+     *
+     * @TODO: Calendar data, more complete test of vCard attribtues.
+     * 
+     */
+    public function testGetMessage()
+    {
+        require_once 'Horde/ActiveSync.php';
+
+        $contact = array(
+            '__key' => '9b07c14b086932e69cc7eb1baed0cc87',
+            '__owner' => 'mike',
+            '__type' => 'Object',
+            '__members' => '',
+            '__uid' => '20100205111228.89913meqtp5u09rg@localhost',
+            'firstname' => 'Michael',
+            'lastname' => 'Rubinsky',
+            'middlenames' => 'Joseph',
+            'namePrefix' => 'Dr',
+            'nameSuffix' => 'PharmD',
+            'name' => 'Michael Joseph Rubinsky',
+            'alias' => 'Me',
+            'birthday' => '1970-03-20',
+            'homeStreet' => '123 Main St.',
+            'homePOBox' => '',
+            'homeCity' => 'Anywhere',
+            'homeProvince' => 'NJ',
+            'homePostalCode' => '08080',
+            'homeCountry' => 'US',
+            'workStreet' => 'Kings Hwy',
+            'workPOBox' => '',
+            'workCity' => 'Somewhere',
+            'workProvince' => 'NJ',
+            'workPostalCode' => '08052',
+            'workCountry' => 'US',
+            'timezone' => 'America/New_York',
+            'email' => 'mrubinsk@horde.org',
+            'homePhone' => '(856)555-1234',
+            'workPhone' => '(856)555-5678',
+            'cellPhone' => '(609)555-9876',
+            'fax' => '',
+            'pager' => '',
+            'title' => '',
+            'role' => '',
+            'company' => '',
+            'category' => '',
+            'notes' => '',
+            'website' => '',
+            'freebusyUrl' => '',
+            'pgpPublicKey' => '',
+            'smimePublicKey' => '',
+        );
+
+        // Need to init the Nls system
+        error_reporting(E_ALL & ~E_DEPRECATED);
+        require_once dirname(__FILE__) . '/../../../../../horde/lib/core.php';
+        Horde_Nls::setLanguage();
+        
+        $fixture = array('contacts_export' => $contact);
+        $connector = new Horde_ActiveSync_MockConnector(array('fixture' => $fixture));
+        $state = $this->getMockSkipConstructor('Horde_ActiveSync_State_File');
+        $driver = new Horde_ActiveSync_Driver_Horde(array('connector' => $connector,
+                                                          'state_basic' => $state));
+
+        $results = $driver->getMessage(Horde_ActiveSync_Driver_Horde::CONTACTS_FOLDER,
+                                       '20070112030603.249j42k3k068@test.theupstairsroom.com',
+                                       0);
+        $this->assertType('Horde_ActiveSync_Message_Contact', $results);
+        $this->assertEquals('Dr', $results->title);
+        $this->assertEquals('PharmD', $results->suffix);
+        $this->assertEquals('Michael Joseph Rubinsky', $results->fileas);
+        $this->assertEquals('mrubinsk@horde.org', $results->email1address);
+        $this->assertEquals('6757200', $results->birthday);
+        $this->assertEquals('(856)555-1234', $results->homephonenumber);
+        $this->assertEquals('(856)555-5678', $results->businessphonenumber);
+        $this->assertEquals('(609)555-9876', $results->mobilephonenumber);
+        $this->assertEquals('123 Main St.', $results->homestreet);
+        $this->assertEquals('Anywhere', $results->homecity);
+        $this->assertEquals('NJ', $results->homestate);
+        $this->assertEquals('08080', $results->homepostalcode);
+        $this->assertEquals('US', $results->homecountry);
+        $this->assertEquals('Kings Hwy', $results->businessstreet);
+        $this->assertEquals('Somewhere', $results->businesscity);
+        $this->assertEquals('NJ', $results->businessstate);
+        $this->assertEquals('08052', $results->businesspostalcode);
+        $this->assertEquals('US', $results->businesscountry);
+    }
+
+    /**
+     * Test that the values present in the contact message are always utf-8.
+     */
+    public function testStreamerUTF8()
+    {
+        // Need to init the Nls system
+        error_reporting(E_ALL & ~E_DEPRECATED);
+        require_once dirname(__FILE__) . '/../../../../../horde/lib/core.php';
+        Horde_Nls::setLanguage();
+        
+        $contact = array(
+            '__uid' => '20100205111228.89913meqtp5u09rg@localhost',
+            'firstname' => Horde_String::convertCharset('Grüb', Horde_Nls::getCharset(), 'iso-8859-1')
+        );
+
+        $fixture = array('contacts_export' => $contact);
+        $connector = new Horde_ActiveSync_MockConnector(array('fixture' => $fixture));
+        $state = $this->getMockSkipConstructor('Horde_ActiveSync_State_File');
+        $driver = new Horde_ActiveSync_Driver_Horde(array('connector' => $connector,
+                                                          'state_basic' => $state));
+
+        $results = $driver->getMessage(Horde_ActiveSync_Driver_Horde::CONTACTS_FOLDER,
+                                       '20070112030603.249j42k3k068@test.theupstairsroom.com',
+                                       0);
+
+        $this->assertEquals(Horde_String::convertCharset('Grüb', Horde_Nls::getCharset(), 'utf-8'), $results->firstname);
+    }
+    /**
+     * Test ChangeMessage:
+     * This tests converting the contact streamer object to a hash suitable for
+     * passing to the contacts/import method. Because it only returns the UID
+     * for the newly added/edited entry, we can't check the results here. The
+     * check is done in the MockConnector object, throwing an exception if it
+     * fails.
+     *
+     */
+    public function testChangeMessage()
+    {
+        // fixtures
+        $message = new Horde_ActiveSync_Message_Contact();
+        $message->fileas = 'Michael Joseph Rubinsky';
+        $message->firstname = 'Michael';
+        $message->lastname = 'Rubinsky';
+        $message->middlename = 'Joseph';
+        $message->birthday = '6757200';
+        $message->email1address = 'mrubinsk@horde.org';
+        $message->homephonenumber = '(856)555-1234';
+        $message->businessphonenumber = '(856)555-5678';
+        $message->mobilephonenumber = '(609)555-9876';
+        $message->homestreet = '123 Main St.';
+        $message->homecity = 'Anywhere';
+        $message->homestate = 'NJ';
+        $message->homepostalcode = '08080';
+
+        // Need to init the Nls system
+        error_reporting(E_ALL & ~E_DEPRECATED);
+        require_once dirname(__FILE__) . '/../../../../../horde/lib/core.php';
+        Horde_Nls::setLanguage();
+        
+        $connector = new Horde_ActiveSync_MockConnector(array('fixture' => array()));
+        $state = $this->getMockSkipConstructor('Horde_ActiveSync_State_File');
+        $driver = new Horde_ActiveSync_Driver_Horde(array('connector' => $connector,
+                                                          'state_basic' => $state));
+
+        try {
+            $results = $driver->ChangeMessage(Horde_ActiveSync_Driver_Horde::CONTACTS_FOLDER,
+                                              0, $message);
+        } catch (Horde_ActiveSync_Exception $e) {
+            $this->fail($e->getMessage());
+        }
+    }
+
+    /**
+     * Test return structure of GetFolderList command
+     */
+    public function testGetFolderList()
+    {
+        $registry = $this->getMockSkipConstructor('Horde_Registry');
+        $state = $this->getMockSkipConstructor('Horde_ActiveSync_State_File');
+        $connector = new Horde_ActiveSync_MockConnector(array('fixture' => array()));
+        $driver = new Horde_ActiveSync_Driver_Horde(array('connector' => $connector,
+                                                          'state_basic' => $state));
+        $results = $driver->getFolderList();
+        $expected = array(
+         array(
+            'id' => 'Calendar',
+            'mod' => 'Calendar',
+            'parent' => 0,
+         ),
+         array(
+            'id' => 'Contacts',
+            'mod' => 'Contacts',
+            'parent' => 0
+        ),
+        array(
+            'id' => 'Tasks',
+            'mod' => 'Tasks',
+            'parent' => 0
+        )
+       );
+
+       $this->assertEquals($expected, $results);
+    }
+}
diff --git a/framework/ActiveSync/test/Horde/ActiveSync/TimezoneTest.php b/framework/ActiveSync/test/Horde/ActiveSync/TimezoneTest.php
new file mode 100644 (file)
index 0000000..319b3f7
--- /dev/null
@@ -0,0 +1,123 @@
+<?php
+/*
+ * Unit tests for Horde_ActiveSync_Timezone utilities
+ *
+ * @author Michael J. Rubinsky <mrubinsk@horde.org>
+ * @category Horde
+ * @package Horde_ActiveSync
+ */
+class Horde_ActiveSync_TimezoneTest extends Horde_Test_Case
+{
+    /**
+     * Test building an Offset hash from a given ActiveSync style base64 encoded
+     * timezone structure.
+     */
+    public function testOffsetsFromSyncTZ()
+    {
+        // America/Los_Angeles GMT-8:00
+        $blob = '4AEAACgARwBNAFQALQAwADgAOgAwADAAKQAgAFAAYQBjAGkAZgBpAGMAIABUAGkAbQBlACAAKABVAFMAIAAmACAAQwAAA AsAAAABAAIAAAAAAAAAAAAAACgARwBNAFQALQAwADgAOgAwADAAKQAgAFAAYQBjAGkAZgBpAGMAIABUAGkAbQBlACAAKA BVAFMAIAAmACAAQwAAAAMAAAACAAIAAAAAAAAAxP///w==';
+        $tz = Horde_ActiveSync_Timezone::getOffsetsFromSyncTZ($blob);
+
+        $expected = array(
+            'bias' => 480,
+            //'stdname' => '(GMT-08:00) Pacific Time (US & C',
+            'stdyear' => 0,
+            'stdmonth' => 11,
+            'stdday' => 0,
+            'stdweek' => 1,
+            'stdhour' => 2,
+            'stdminute' => 0,
+            'stdsecond' => 0,
+            'stdmillis' => 0,
+            'stdbias' => 0,
+            'dstyear' => 0,
+            'dstmonth' => 3,
+            'dstday' => 0,
+            'dstweek' => 2,
+            'dsthour' => 2,
+            'dstminute' => 0,
+            'dstsecond' => 0,
+            'dstmillis' => 0,
+            'dstbias' => -60,
+            'timezone' => 480,
+            'timezonedst' => -60
+        );
+
+        foreach ($expected as $key => $value) {
+            $this->assertEquals($value, $tz[$key]);
+        }
+    }
+
+    /**
+     * Test creating a Offset hash for a given timezone.
+     */
+    public function testGetOffsetsFromDate()
+    {
+        // The actual time doesn't matter, we really only need a year and a
+        // timezone that we are interested in.
+        $date = new Horde_Date(time(), 'America/Los_Angeles');
+        $tz = Horde_ActiveSync_Timezone::getOffsetsFromDate($date);
+        
+        /* We don't set the name here */
+        $expected = array(
+            'bias' => 480,
+            'stdname' => '',
+            'stdyear' => 0,
+            'stdmonth' => 11,
+            'stdday' => 0,
+            'stdweek' => 1,
+            'stdhour' => 2,
+            'stdminute' => 0,
+            'stdsecond' => 0,
+            'stdmillis' => 0,
+            'stdbias' => 0,
+            'dstname' => '',
+            'dstyear' => 0,
+            'dstmonth' => 3,
+            'dstday' => 0,
+            'dstweek' => 2,
+            'dsthour' => 2,
+            'dstminute' => 0,
+            'dstsecond' => 0,
+            'dstmillis' => 0,
+            'dstbias' => -60,
+        );
+
+        foreach ($expected as $key => $value) {
+            $this->assertEquals($value, $tz[$key]);
+        }
+    }
+
+    /**
+     * Test generating an ActiveSync TZ structure given a TZ Offset hash
+     */
+    public function testGetSyncTZFromOffsets()
+    {
+         $offsets = array(
+            'bias' => 480,
+            'stdname' => '',
+            'stdyear' => 0,
+            'stdmonth' => 11,
+            'stdday' => 0,
+            'stdweek' => 1,
+            'stdhour' => 2,
+            'stdminute' => 0,
+            'stdsecond' => 0,
+            'stdmillis' => 0,
+            'stdbias' => 0,
+            'dstname' => '',
+            'dstyear' => 0,
+            'dstmonth' => 3,
+            'dstday' => 0,
+            'dstweek' => 2,
+            'dsthour' => 2,
+            'dstminute' => 0,
+            'dstsecond' => 0,
+            'dstmillis' => 0,
+            'dstbias' => -60,
+        );
+
+        $tz = Horde_ActiveSync_Timezone::getSyncTZFromOffsets($offsets);
+        $this->assertEquals('4AEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAsAAAABAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAACAAIAAAAAAAAAxP///w==', $tz);
+    }
+}
\ No newline at end of file
diff --git a/framework/ActiveSync/test/Horde/ActiveSync/fixtures/MockConnector.php b/framework/ActiveSync/test/Horde/ActiveSync/fixtures/MockConnector.php
new file mode 100644 (file)
index 0000000..5e9456b
--- /dev/null
@@ -0,0 +1,76 @@
+<?php
+/**
+ * Mock connector for unit testing horde backend.
+ *
+ * @author Michael J. Rubinsky <mrubinsk@horde.org>
+ * @category Horde
+ * @package Horde_ActiveSync
+ */
+class Horde_ActiveSync_MockConnector
+{
+    public function __construct($params = array())
+    {
+        $this->_fixture = $params['fixture'];
+    }
+
+    public function __call($name, $args)
+    {
+        if (empty($this->_fixture[$name])) {
+            return 0;
+        }
+        return $this->_fixture[$name];
+    }
+
+    public function contacts_import($content, $content_type)
+    {
+        // $content is a vCard that should eq:
+        $expected = array(
+            'firstname' => 'Michael',
+            'lastname' => 'Rubinsky',
+            'middlenames' => 'Joseph',
+            'namePrefix' => '',
+            'nameSuffix' => '',
+            'name' => 'Michael Joseph Rubinsky',
+            'birthday' => '1970-03-20',
+            'homeStreet' => '123 Main St.',
+            'homeCity' => 'Anywhere',
+            'homeProvince' => 'NJ',
+            'homePostalCode' => '08080',
+            'homeCountry' => '',
+            'workStreet' => '',
+            'workCity' => '',
+            'workProvince' => '',
+            'workCountry' => '',
+            //'timezone' => '',
+            'email' => 'mrubinsk@horde.org',
+            'homePhone' => '(856)555-1234',
+            'workPhone' => '(856)555-5678',
+            'cellPhone' => '(609)555-9876',
+            'fax' => '',
+            'pager' => '',
+            'title' => '',
+            'company' => '',
+            //'category' => '',
+            'notes' => '',
+            'website' => '',
+        );
+
+        foreach ($expected as $key => $value) {
+            if ($content[$key] != $value) {
+                throw new Horde_ActiveSync_Exception('Expected value ' . $value . ' did not match received value ' . $content[$key]);
+            }
+        }
+
+        return 'xx.xx@localhost';
+    }
+
+    public function contacts_replace()
+    {
+        
+    }
+
+    public function calendar_import()
+    {
+
+    }
+}
\ No newline at end of file
index a72596d..e5103d7 100644 (file)
@@ -476,6 +476,7 @@ class Horde_Date
         } else {
             $d->sec += $factor;
         }
+
         return $d;
     }
 
index e33bdfb..8fdc5db 100644 (file)
@@ -50,23 +50,51 @@ class Horde_Rpc
     var $_requestMissingAuthorization = true;
 
     /**
+     * Request variables, cookies etc...
+     *
+     * @var Horde_Controller_Request_Http
+     */
+    protected $_request;
+
+    /**
+     * Logging
+     *
+     * @var Horde_Log_Logger
+     */
+    protected $_logger;
+
+    /**
      * RPC server constructor.
      *
+     * @param Horde_Controller_Request_Http  The request object
+     *
      * @param array $config  A hash containing any additional configuration or
      *                       connection parameters a subclass might need.
      *
      * @return Horde_Rpc  An RPC server instance.
      */
-    public function __construct($params = array())
+    public function __construct($request, $params = array())
     {
-        $this->_params = $params;
+        // Create a stub if we don't have a useable logger.
+        if (isset($params['logger'])
+            && is_callable(array($params['logger'], 'log'))) {
+            $this->_logger = $params['logger'];
+            unset($params['logger']);
+        } else {
+            $this->_logger = new Horde_Support_Stub;
+        }
 
+        $this->_params = $params;
+        $this->_request = $request;
+        
         if (isset($params['requireAuthorization'])) {
             $this->_requireAuthorization = $params['requireAuthorization'];
         }
         if (isset($params['requestMissingAuthorization'])) {
             $this->_requestMissingAuthorization = $params['requestMissingAuthorization'];
         }
+
+        $this->_logger->debug('Horde_Rpc::__construct complete');
     }
 
     /**
@@ -81,17 +109,19 @@ class Horde_Rpc
      */
     function authorize()
     {
+        $this->_logger->debug('Horde_Rpc::authorize() starting');
         if (!$this->_requireAuthorization) {
             return true;
         }
 
+        // @TODO: inject this
         $auth = Horde_Auth::singleton($GLOBALS['conf']['auth']['driver']);
 
-        if (isset($_SERVER['PHP_AUTH_USER'])) {
-            $user = $_SERVER['PHP_AUTH_USER'];
-            $pass = $_SERVER['PHP_AUTH_PW'];
-        } elseif (isset($_SERVER['Authorization'])) {
-            $hash = str_replace('Basic ', '', $_SERVER['Authorization']);
+        if ($this->_request->getServer('PHP_AUTH_USER')) {
+            $user = $this->_request->getServer('PHP_AUTH_USER');
+            $pass = $this->_request->getServer('PHP_AUTH_PW');
+        } elseif ($this->_request->getServer('Authorization')) {
+            $hash = str_replace('Basic ', '', $this->_request->getServer('Authorization'));
             $hash = base64_decode($hash);
             if (strpos($hash, ':') !== false) {
                 list($user, $pass) = explode(':', $hash, 2);
@@ -108,6 +138,7 @@ class Horde_Rpc
             exit;
         }
 
+        $this->_logger->debug('Horde_Rpc::authorize() exiting');
         return true;
     }
 
@@ -154,6 +185,22 @@ class Horde_Rpc
     }
 
     /**
+     * Send the output back to the client
+     *
+     * @param string $output  The output to send back to the client. Can be
+     *                        overridden in classes if needed.
+     *
+     * @return void
+     */
+    function sendOutput($output)
+    {
+        header('Content-Type: ' . $this->getResponseContentType());
+        header('Content-length: ' . strlen($output));
+        header('Accept-Charset: UTF-8');
+        echo $output;
+    }
+
+    /**
      * Builds an RPC request and sends it to the RPC server.
      *
      * This statically called method is actually the RPC client.
@@ -192,12 +239,12 @@ class Horde_Rpc
      * @return Horde_Rpc  The newly created concrete Horde_Rpc server instance,
      *                    or an exception if there is an error.
      */
-    public static function factory($driver, $params = null)
+    public static function factory($driver, $request, $params = null)
     {
         $driver = basename($driver);
         $class = 'Horde_Rpc_' . $driver;
         if (class_exists($class)) {
-            return new $class($params);
+            return new $class($request, $params);
         } else {
             throw new Horde_Rpc_Exception('Class definition of ' . $class . ' not found.');
         }
diff --git a/framework/Rpc/lib/Horde/Rpc/ActiveSync.php b/framework/Rpc/lib/Horde/Rpc/ActiveSync.php
new file mode 100644 (file)
index 0000000..7e8d5ea
--- /dev/null
@@ -0,0 +1,264 @@
+<?php
+/**
+ * Copyright 2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author   Michael J. Rubinsky <mrubinsk@horde.org>
+ *
+ * @category Horde
+ * @package  Horde_Rpc
+ */
+class Horde_Rpc_ActiveSync extends Horde_Rpc
+{
+    /**
+     * Holds the request's GET variables
+     *
+     * @var array
+     */
+    private $_get;
+
+    /**
+     * The ActiveSync server object
+     *
+     * @var Horde_ActiveSync
+     */
+    private $_server;
+
+    /**
+     * ActiveSync's backend target (the datastore it syncs the PDA with)
+     *
+     * @var Horde_ActiveSync_Driver
+     */
+    private $_backend;
+
+    /**
+     * Policy key
+     * (Sent by client either as 'X-Ms-Policykey' or 'X-MS-PolicyKey')
+     *
+     * @var string
+     */
+    private $_policykey;
+
+    /**
+     * ActiveSync protocol version
+     * (Sent as either 'Ms-Asprotocolversion' or 'MS-ASProtocolVersion')
+     * Should default to '1.0' if not sent.
+     *
+     * @var string
+     */
+    private $_protocolVersion;
+
+    /**
+     * Require provisioning? Valid values:
+     *  true  - provisioning required
+     *  false - not checked
+     *  loose - Allows the 'loose enforcement' of the provisioning policies for
+     *          older devices which don't support provisioning
+     *
+     * @var mixed
+     */
+    private $_provisioning;
+
+    /**
+     * Constructor.
+     * Parameters in addition to Horde_Rpc's:
+     *   (required) 'backend'      = Horde_ActiveSync_Driver
+     *   (required) 'server'       = Horde_ActiveSync
+     *   (optional) 'provisioning' = Require device provisioning?
+     *
+     * @param Horde_Controller_Request_Http  The request object.
+     * @param array $config  A hash containing any additional configuration or
+     *                       connection parameters this class might need.
+     */
+    public function __construct($request, $params = array())
+    {
+        parent::__construct($request, $params);
+
+        /* Check for requirements */
+        $this->_get = $request->getGetParams();
+        if ($request->getMethod() == 'POST' &&
+            (empty($this->_get['Cmd']) ||  empty($this->_get['User']) || empty($this->_get['DeviceId']) || empty($this->_get['DeviceType']))) {
+
+            $this->_logger->err('Missing required parameters.');
+
+            throw new Horde_Rpc_Exception('Your device requested the ActiveSync URL wihtout required parameters.');
+        }
+
+        /* Set our server and backend objects */
+        $this->_backend = $params['backend'];
+        $this->_server = $params['server'];
+
+        /* provisioning can be false, true, or 'loose' */
+        $this->_provisioning = empty($params['provisioning']) ? false : $params['provisioning'];
+        if ($this->_provisioning) {
+            $this->_policykey = $this->_server->getPolicyKey();
+        }
+        $this->_server->setProvisioning = $this->_provisioning;
+
+        /* Protocol Version */
+        $this->_protocolVersion = $this->_server->getProtocolVersion();
+    }
+
+    /**
+     * Returns the Content-Type of the response.
+     *
+     * @return string  The MIME Content-Type of the RPC response.
+     */
+    public function getResponseContentType()
+    {
+        return 'application/vnd.ms-sync.wbxml';
+    }
+
+    /**
+     * Horde_ActiveSync will read the input stream directly, do not access
+     * it here.
+     *
+     * @see framework/Rpc/lib/Horde/Horde_Rpc#getInput()
+     */
+    public function getInput()
+    {
+        return null;
+    }
+
+    /**
+     * Sends an RPC request to the server and returns the result.
+     *
+     * @param string $request  PHP input stream (ignored).
+     *
+     * @return void
+     */
+    public function getResponse($request)
+    {
+        /* Not sure about this, but it's what zpush did so... */
+        ob_start(null, 1048576);
+
+        switch ($this->_request->getServer('REQUEST_METHOD')) {
+        case 'OPTIONS':
+            $this->_logger->debug('Horde_Rpc_ActiveSync::getResponse() starting for OPTIONS');
+            $this->_server->handleRequest('Options', null, null);
+            break;
+
+        case 'POST':
+            Horde_ActiveSync::activeSyncHeader();
+
+            // Do the actual request
+            $this->_logger->debug('Horde_Rpc_ActiveSync::getResponse() starting for ' . $this->_get['Cmd']);
+            if (!$this->_server->handleRequest($this->_get['Cmd'], $this->_get['DeviceId'], $this->_protocolVersion)) {
+                /* @TODO If request failed, try to output a reasonable error to the
+                 * device if we can...and this should be done from the ActiveSync
+                 * objects anyway...
+                 */
+                if(!headers_sent()) {
+                    header('Content-type: text/html');
+                    echo("<BODY>\n");
+                    echo("<h3>Error</h3><p>\n");
+                    echo("There was a problem processing the <i>{$this->_get['Cmd']}</i> command from your PDA.\n");
+                    echo("</BODY>\n");
+                }
+            }
+            break;
+
+        case 'GET':
+            /* Someone trying to access the activesync url from a browser */
+            throw new Horde_Rpc_Exception('Trying to access the ActiveSync endpoint from a browser. Not Supported.');
+            break;
+        }
+    }
+
+    /**
+     *
+     * @see framework/Rpc/lib/Horde/Horde_Rpc#sendOutput($output)
+     */
+    public function sendOutput($output)
+    {
+        // Unfortunately, even though zpush can stream the data to the client
+        // with a chunked encoding, using chunked encoding also breaks the
+        // progress bar on the PDA. So we de-chunk here and just output a
+        // content-length header and send it as a 'normal' packet. If the output
+        // packet exceeds 1MB (see ob_start) then it will be sent as a chunked
+        // packet anyway because PHP will have to flush the buffer.
+        $len = ob_get_length();
+        $data = ob_get_contents();
+        ob_end_clean();
+
+        // TODO: Figure this out...Z-Push had two possible paths for outputting
+        // to the client, 1) if the ob reached it's capacity, and here...but
+        // it didn't originally output the Content-Type header
+        header('Content-Type: application/vnd.ms-sync.wbxml');
+        header('Content-Length: ' . $len);
+        echo $data;
+    }
+
+    /**
+     * Check authentication. Different backends may handle
+     * authentication in different ways. The base class implementation
+     * checks for HTTP Authentication against the Horde auth setup.
+     *
+     * @TODO should the realm be configurable - since Horde is only one of the
+     * possible backends?
+     *
+     * @return boolean  Returns true if authentication is successful.
+     *                  Should send appropriate "not authorized" headers
+     *                  or other response codes/body if auth fails,
+     *                  and take care of exiting.
+     */
+    public function authorize()
+    {
+        $this->_logger->debug('Horde_Rpc_ActiveSync::authorize() starting');
+        if (!$this->_requireAuthorization) {
+            return true;
+        }
+
+        /* Get user and possibly domain */
+        $user = $this->_request->getServer('PHP_AUTH_USER');
+        $pos = strrpos($user, '\\');
+        if ($pos !== false) {
+            $domain = substr($user, 0, $pos);
+            $user = substr($user, $pos + 1);
+        } else {
+            $domain = null;
+        }
+
+        /* Get passwd */
+        $pass = $this->_request->getServer('PHP_AUTH_PW');
+
+        /* Attempt to auth to backend */
+        $results = $this->_backend->logon($user, $pass, $domain);
+        if (!$results && empty($this->_policykey)) {
+            header('HTTP/1.1 401 Unauthorized');
+            header('WWW-Authenticate: Basic realm="Horde RPC"');
+            echo 'Access denied. Username or password incorrect.';
+            // @TODO: Logging, once Rpc gets an actual logger.
+        }
+
+        /* Successfully authenticated to backend, try to setup the backend */
+        if (empty($this->_get['User'])) {
+            return false;
+        }
+        $results = $this->_backend->setup($this->_get['User']);
+        if (!$results) {
+            header('HTTP/1.1 401 Unauthorized');
+            header('WWW-Authenticate: Basic realm="Horde RPC"');
+            echo 'Access denied or user ' . $this->_get['User'] . ' unknown.';
+        }
+
+        /* Policies / Provisioning */
+        if ($this->_provisioning !== false && $this->_request->getServer('REQUEST_METHOD') != 'OPTIONS' &&
+            $this->_get['Cmd'] != 'Ping' && $this->_get['Cmd'] != 'Provision' &&
+            $this->_backend->CheckPolicy($this->_policykey, $this->_get['DeviceId']) != SYNC_PROVISION_STATUS_SUCCESS &&
+            ($this->_provisioning !== 'loose' ||
+            ($this->_provisioning === 'loose' && !empty($this->_policyKey)))) {
+
+            Horde_ActiveSync::provisioningRequired();
+
+            return false;
+        }
+
+        $this->_logger->debug('Horde_Rpc_ActiveSync::authorize() exiting');
+
+        return true;
+    }
+
+}
index 7e5b550..c5d1703 100644 (file)
@@ -31,9 +31,9 @@ class Horde_Rpc_Jsonrpc extends Horde_Rpc
      * @param array $config  A hash containing any additional configuration or
      *                       connection parameters this class might need.
      */
-    function __construct($params = array())
+    function __construct($request, $params = array())
     {
-        parent::__construct($params);
+        parent::__construct($request, $params);
         Horde_Nls::setCharsetEnvironment('UTF-8');
     }
 
index 1b8d288..85da702 100644 (file)
@@ -23,9 +23,9 @@ class Horde_Rpc_Phpgw extends Horde_Rpc
     /**
      * XMLRPC server constructor.
      */
-    function __construct()
+    function __construct($request, $params = array())
     {
-        parent::__construct();
+        parent::__construct($request, $params);
 
         $this->_server = xmlrpc_server_create();
 
index 35ab32f..904ec54 100644 (file)
@@ -46,11 +46,11 @@ class Horde_Rpc_Soap extends Horde_Rpc
      *
      * @access private
      */
-    public function __construct($params = array())
+    public function __construct($request, $params = array())
     {
         Horde_Nls::setCharset('UTF-8');
 
-        parent::__construct($params);
+        parent::__construct($request, $params);
 
         if (!empty($params['allowedTypes'])) {
             $this->_allowedTypes = $params['allowedTypes'];
index 14d87cc..6c7dc13 100644 (file)
@@ -216,7 +216,7 @@ class Horde_Rpc_Webdav extends Horde_Rpc
      *
      * @access private
      */
-    public function __construct()
+    public function __construct($request, $params = array())
     {
         // PHP messages destroy XML output -> switch them off
         ini_set('display_errors', 0);
@@ -225,7 +225,7 @@ class Horde_Rpc_Webdav extends Horde_Rpc
         // so that derived classes can simply modify these
         $this->_SERVER = $_SERVER;
 
-        parent::__construct();
+        parent::__construct($request, $params);
     }
 
     /**
@@ -2381,11 +2381,11 @@ class Horde_Rpc_Webdav extends Horde_Rpc
     {
         $args = func_get_args();
         if (count($args) == 3) {
-            return array("ns"   => $args[0], 
+            return array("ns"   => $args[0],
                          "name" => $args[1],
                          "val"  => $args[2]);
         } else {
-            return array("ns"   => "DAV:", 
+            return array("ns"   => "DAV:",
                          "name" => $args[0],
                          "val"  => $args[1]);
         }
index cfa73cc..3105c27 100644 (file)
@@ -25,9 +25,9 @@ class Horde_Rpc_Xmlrpc extends Horde_Rpc
      *
      * @access private
      */
-    public function __construct()
+    public function __construct($request, $params = array())
     {
-        parent::__construct();
+        parent::__construct($request, $params);
 
         $this->_server = xmlrpc_server_create();
 
index 7517c75..a734194 100644 (file)
@@ -49,6 +49,7 @@ various remote methods of accessing Horde functionality.
       <file name="Syncml.php" role="php" />
       <file name="Webdav.php" role="php" />
       <file name="Xmlrpc.php" role="php" />
+      <file name="ActiveSync.php" role="php" />
      </dir> <!-- /lib/Horde/Rpc -->
      <file name="Rpc.php" role="php" />
     </dir> <!-- /lib/Horde -->
@@ -86,6 +87,7 @@ various remote methods of accessing Horde functionality.
    <install name="lib/Horde/Rpc/Syncml.php" as="Horde/Rpc/Syncml.php" />
    <install name="lib/Horde/Rpc/Webdav.php" as="Horde/Rpc/Webdav.php" />
    <install name="lib/Horde/Rpc/Xmlrpc.php" as="Horde/Rpc/Xmlrpc.php" />
+   <install name="lib/Horde/Rpc/ActiveSync.php" as="Horde/Rpc/ActiveSync.php" />
    <install name="lib/Horde/Rpc.php" as="Horde/Rpc.php" />
   </filelist>
  </phprelease>
index bc01586..ef7f284 100644 (file)
@@ -125,7 +125,7 @@ class Horde_iCalendar_vcard extends Horde_iCalendar {
      *
      * @return string  The RFC822-formatted email address.
      */
-    function getBareEmail($address)
+    static function getBareEmail($address)
     {
         // Empty values are still empty.
         if (!$address) {
index 852435f..e413e9f 100644 (file)
    </configswitch>
   </configsection>
  </configtab>
+ <configtab name="activesync" desc="ActiveSync">
+  <configsection name="activesync">
+   <configswitch name="enabled" quote="false" desc="Enable ActiveSync server?">false
+    <case name="false" desc="Disabled" />
+    <case name="true" desc="Enabled">
+     <configsection name="state">
+      <configheader>State Storage Settings</configheader>
+      <configstring name="directory" desc="Directory to hold state files:">/tmp</configstring>
+     </configsection>
+    </case>
+   </configswitch>
+  </configsection>
+ </configtab>
 </configuration>
index c978bf6..a0ad65e 100644 (file)
@@ -23,23 +23,52 @@ $input = $session_control = null;
 $nocompress = false;
 $params = array();
 
+/* Load base libraries. */
+Horde_Registry::appInit('horde', array('authentication' => 'none', 'nocompress' => $nocompress, 'session_control' => $session_control));
+
+/* Get a request object. */
+$request = new Horde_Controller_Request_Http();
+
+/* TODO: This is for debugging, replace with logger from injector before merge */
+$params['logger'] = new Horde_Log_Logger(new Horde_Log_Handler_Stream(fopen('/tmp/activesync.txt', 'a')));
+
 /* Look at the Content-type of the request, if it is available, to try
  * and determine what kind of request this is. */
-if (!empty($_SERVER['PATH_INFO']) ||
-    in_array($_SERVER['REQUEST_METHOD'], array('DELETE', 'PROPFIND', 'PUT', 'OPTIONS'))) {
+if (!empty($GLOBALS['conf']['activesync']['enabled']) && 
+    ((strpos($request->getServer('CONTENT_TYPE'), 'application/vnd.ms-sync.wbxml') !== false) ||
+    (strpos($request->getUri(), 'Microsoft-Server-ActiveSync') !== false))) {
+    /* ActiveSync Request */
+    $serverType = 'ActiveSync';
+    $horde_session_control = 'none';
+    $horde_no_compress = true;
+
+    /* TODO: Probably want to bind a factory to injector for this? */
+    $params['registry'] = $GLOBALS['registry'];
+    $connector = new Horde_ActiveSync_Driver_Horde_Connector_Registry($params);
+    $stateMachine = new Horde_ActiveSync_State_File(array('stateDir' => $GLOBALS['conf']['activesync']['state']['directory']));
+    $params['backend'] = new Horde_ActiveSync_Driver_Horde(array('connector' => $connector,
+                                                                 'state_basic' => $stateMachine));
+    $params['server'] = new Horde_ActiveSync($params['backend'],
+                                             new Horde_ActiveSync_Wbxml_Decoder(fopen('php://input', 'r'), Horde_ActiveSync::$zpushdtd),
+                                             new Horde_ActiveSync_Wbxml_Encoder(fopen('php://output', 'w+'), Horde_ActiveSync::$zpushdtd),
+                                             $request);
+    $params['server']->setLogger($params['logger']);
+
+} elseif ($request->getServer('PATH_INFO') ||
+    in_array($request->getServer('REQUEST_METHOD'), array('DELETE', 'PROPFIND', 'PUT', 'OPTIONS'))) {
     $serverType = 'Webdav';
-} elseif (!empty($_SERVER['CONTENT_TYPE'])) {
-    if (strpos($_SERVER['CONTENT_TYPE'], 'application/vnd.syncml+xml') !== false) {
+} elseif ($request->getServer('CONTENT_TYPE')) {
+    if (strpos($request->getServer('CONTENT_TYPE'), 'application/vnd.syncml+xml') !== false) {
         $serverType = 'Syncml';
         /* Syncml does its own session handling. */
         $session_control = 'none';
         $nocompress = true;
-    } elseif (strpos($_SERVER['CONTENT_TYPE'], 'application/vnd.syncml+wbxml') !== false) {
+    } elseif (strpos($request->getServer('CONTENT_TYPE'), 'application/vnd.syncml+wbxml') !== false) {
         $serverType = 'Syncml_Wbxml';
         /* Syncml does its own session handling. */
-        $session_control = 'none';
-        $nocompress = true;
-    } elseif (strpos($_SERVER['CONTENT_TYPE'], 'text/xml') !== false) {
+        $horde_session_control = 'none';
+        $horde_no_compress = true;
+    } elseif (strpos($request->getServer('CONTENT_TYPE'), 'text/xml') !== false) {
         $input = Horde_Rpc::getInput();
         /* Check for SOAP namespace URI. */
         if (strpos($input, 'http://schemas.xmlsoap.org/soap/envelope/') !== false) {
@@ -47,21 +76,21 @@ if (!empty($_SERVER['PATH_INFO']) ||
         } else {
             $serverType = 'Xmlrpc';
         }
-    } elseif (strpos($_SERVER['CONTENT_TYPE'], 'application/json') !== false) {
+    } elseif (strpos($request->getServer('CONTENT_TYPE'), 'application/json') !== false) {
         $serverType = 'Jsonrpc';
     } else {
         header('HTTP/1.0 501 Not Implemented');
         exit;
     }
-} elseif (!empty($_SERVER['QUERY_STRING']) && $_SERVER['QUERY_STRING'] == 'phpgw') {
+} elseif ($request->getServer('QUERY_STRING') && $request->getServer('QUERY_STRING') == 'phpgw') {
     $serverType = 'Phpgw';
 } else {
     $serverType = 'Soap';
 }
 
 if ($serverType == 'Soap' &&
-    (!isset($_SERVER['REQUEST_METHOD']) ||
-     $_SERVER['REQUEST_METHOD'] != 'POST')) {
+    (!$request->getServer('REQUEST_METHOD') ||
+     $request->getServer('REQUEST_METHOD') != 'POST')) {
     $session_control = 'none';
     $params['requireAuthorization'] = false;
     if (Horde_Util::getGet('wsdl') !== null) {
@@ -77,11 +106,8 @@ if (($ra = Horde_Util::getGet('requestMissingAuthorization')) !== null) {
     $params['requestMissingAuthorization'] = $ra;
 }
 
-/* Load base libraries. */
-Horde_Registry::appInit('horde', array('authentication' => 'none', 'nocompress' => $nocompress, 'session_control' => $session_control));
-
 /* Load the RPC backend based on $serverType. */
-$server = Horde_Rpc::factory($serverType, $params);
+$server = Horde_Rpc::factory($serverType, $request, $params);
 
 /* Let the backend check authentication. By default, we look for HTTP
  * basic authentication against Horde, but backends can override this
@@ -101,8 +127,6 @@ if (is_a($out, 'PEAR_Error')) {
     exit;
 }
 
-/* Return the response to the client. */
-header('Content-Type: ' . $server->getResponseContentType());
-header('Content-length: ' . strlen($out));
-header('Accept-Charset: UTF-8');
-echo $out;
+// Allow backends to determine how and when to send output.
+$server->sendOutput($out);
+
index 01e77f1..ec6e192 100644 (file)
@@ -87,12 +87,14 @@ if ($exception = Horde_Util::getFormData('del_exception')) {
                                                  $exception->month,
                                                  $exception->mday);
                 $event->save();
+                $uid = $event->uid;
 
                 /* Create one-time event. */
                 $kronolith_driver->open($target);
                 $event = $kronolith_driver->getEvent();
                 $event->readForm();
                 $event->recurrence->setRecurType(Horde_Date_Recurrence::RECUR_NONE);
+                $event->baseid = $uid;
 
                 break;
 
index 5467d8e..c13b29b 100644 (file)
@@ -552,6 +552,8 @@ class Kronolith_Api extends Horde_Registry_Api
             throw new Horde_Exception_PermissionDenied();
         }
 
+        $kronolith_driver = Kronolith::getDriver(null, $calendar);
+
         switch ($contentType) {
         case 'text/calendar':
         case 'text/x-vcalendar':
@@ -569,7 +571,6 @@ class Kronolith_Api extends Horde_Registry_Api
                 throw new Kronolith_Exception(_("No iCalendar data was found."));
             }
 
-            $kronolith_driver = Kronolith::getDriver(null, $calendar);
             $ids = array();
             foreach ($components as $content) {
                 if ($content instanceof Horde_iCalendar_vevent) {
@@ -608,6 +609,12 @@ class Kronolith_Api extends Horde_Registry_Api
                 return $ids[0];
             }
             return $ids;
+
+            case 'activesync':
+                $event = $kronolith_driver->getEvent();
+                $event->fromASAppointment($content);
+                $event->save();
+                return $event->uid;
         }
 
         throw new Kronolith_Exception(sprintf(_("Unsupported Content-Type: %s"), $contentType));
@@ -679,6 +686,8 @@ class Kronolith_Api extends Horde_Registry_Api
 
             return $iCal->exportvCalendar();
 
+        case 'activesync':
+            return $event->toASAppointment();
         }
 
         throw new Kronolith_Exception(sprintf(_("Unsupported Content-Type: %s"), $contentType));
@@ -820,6 +829,11 @@ class Kronolith_Api extends Horde_Registry_Api
 
         if ($content instanceof Horde_iCalendar_vevent) {
             $component = $content;
+        } elseif ($content instanceof Horde_ActiveSync_Message_Appointment) {
+            $event->fromASAppointment($content);
+            $event->save();
+            $event->uid = $uid;
+            return;
         } else {
             switch ($contentType) {
             case 'text/calendar':
@@ -1000,12 +1014,16 @@ class Kronolith_Api extends Horde_Registry_Api
      * @param boolean $showRemote      Return events from remote calendars and
      *                                 listTimeObject API as well?
      *
+     * @param boolean $hideExceptions  Hide events that represent exceptions to
+     *                                 a recurring event (events with baseid
+     *                                 set)?
+     *
      * @return array  A list of event hashes.
      * @throws Kronolith_Exception
      */
     public function listEvents($startstamp = null, $endstamp = null,
         $calendars = null, $showRecurrence = true,
-        $alarmsOnly = false, $showRemote = true)
+        $alarmsOnly = false, $showRemote = true, $hideExceptions = false)
     {
         if (!isset($calendars)) {
             $calendars = array($GLOBALS['prefs']->getValue('default_share'));
@@ -1019,9 +1037,14 @@ class Kronolith_Api extends Horde_Registry_Api
             }
         }
 
-        return Kronolith::listEvents(new Horde_Date($startstamp),
+        return Kronolith::listEvents(
+            new Horde_Date($startstamp),
             new Horde_Date($endstamp),
-            $calendars, $showRecurrence, $alarmsOnly, $showRemote);
+            $calendars,
+            $showRecurrence,
+            $alarmsOnly,
+            $showRemote,
+            $hideExceptions);
     }
 
     /**
index dbe82cf..6ad387f 100644 (file)
@@ -146,6 +146,17 @@ class Kronolith_Driver_Sql extends Kronolith_Driver
                 }
             }
         }
+
+        if (!empty($query->baseid)) {
+            $binds = Horde_SQL::buildClause($this->_db, 'event_baseid', '=', $query->baseid, true);
+            if (is_array($binds)) {
+                $cond .= $binds[0] . ' AND ';
+                $values = array_merge($values, $binds[1]);
+            } else {
+                $cond .= $binds;
+            }
+        }
+
         if (isset($query->status)) {
             $binds = Horde_SQL::buildClause($this->_db, 'event_status', '=', $query->status, true);
             if (is_array($binds)) {
@@ -266,7 +277,7 @@ class Kronolith_Driver_Sql extends Kronolith_Driver
      */
     public function listEvents($startDate = null, $endDate = null,
                                $showRecurrence = false, $hasAlarm = false,
-                               $json = false, $coverDates = true)
+                               $json = false, $coverDates = true, $hideExceptions = false)
     {
         if (!is_null($startDate)) {
             $startDate = clone $startDate;
@@ -278,9 +289,16 @@ class Kronolith_Driver_Sql extends Kronolith_Driver
             $endDate->min = $endDate->sec = 59;
         }
 
-        $events = $this->_listEventsConditional($startDate, $endDate,
-                                                $hasAlarm ? 'event_alarm > ?' : '',
-                                                $hasAlarm ? array(0) : array());
+        $conditions =  $hasAlarm ? 'event_alarm > ?' : '';
+        $values = $hasAlarm ? array(0) : array();
+        if ($hideExceptions) {
+            if (!empty($conditions)) {
+                $conditions .= ' AND ';
+            }
+            $conditions .= "event_baseid = ''";
+        }
+
+        $events = $this->_listEventsConditional($startDate, $endDate, $conditions, $values);
         $results = array();
         foreach ($events as $id) {
             Kronolith::addEvents($results, $this->getEvent($id), $startDate,
@@ -312,7 +330,7 @@ class Kronolith_Driver_Sql extends Kronolith_Driver
             ' event_recurtype, event_recurenddate, event_recurinterval,' .
             ' event_recurdays, event_start, event_end, event_allday,' .
             ' event_alarm, event_alarm_methods, event_modified,' .
-            ' event_exceptions, event_creator_id, event_resources' .
+            ' event_exceptions, event_creator_id, event_resources, event_baseid' .
             ' FROM ' . $this->_params['table'] .
             ' WHERE calendar_id = ?';
         $values = array($this->calendar);
@@ -426,8 +444,9 @@ class Kronolith_Driver_Sql extends Kronolith_Driver
             ' event_recurtype, event_recurenddate, event_recurinterval,' .
             ' event_recurdays, event_start, event_end, event_allday,' .
             ' event_alarm, event_alarm_methods, event_modified,' .
-            ' event_exceptions, event_creator_id, event_resources' .
-            ' FROM ' . $this->_params['table'] . ' WHERE event_id = ? AND calendar_id = ?';
+            ' event_exceptions, event_creator_id, event_resources,' .
+            ' event_baseid FROM ' . $this->_params['table'] .
+            ' WHERE event_id = ? AND calendar_id = ?';
         $values = array($eventId, $this->calendar);
 
         /* Log the query at a DEBUG log level. */
index a63ff09..0f303be 100644 (file)
@@ -269,6 +269,14 @@ abstract class Kronolith_Event
     protected $_rowspan;
 
     /**
+     * The baseid. For events that represent exceptions this is the UID of the
+     * original, recurring event.
+     *
+     * @var string
+     */
+    public $baseid;
+
+    /**
      * Constructor.
      *
      * @param Kronolith_Driver $driver  The backend driver that this event is
@@ -966,6 +974,176 @@ abstract class Kronolith_Event
     }
 
     /**
+     * Imports the values for this event from a MS ActiveSync Message.
+     *
+     * @see Horde_ActiveSync_Message_Appointment
+     */
+    public function fromASAppointment(Horde_ActiveSync_Message_Appointment $message)
+    {
+        /* New event? */
+        if ($this->id === null) {
+            $this->creator = Horde_Auth::getAuth();
+        }
+        if ($title = Horde_String::convertCharset($message->getSubject(), 'utf-8', Horde_Nls::getCharset())) {
+            $this->title = $title;
+        }
+        if ($description = Horde_String::convertCharset($message->getBody(), 'utf-8', Horde_Nls::getCharset())) {
+            $this->description = $description;
+        }
+        if ($location = Horde_String::convertCharset($message->getLocation(), 'utf-8', Horde_Nls::getCharset())) {
+            $this->location = $location;
+        }
+
+        /* Date/times */
+        $dates = $message->getDatetime();
+        $this->start = $dates['start'];
+        $this->end = $dates['end'];
+        $this->allday = $dates['allday'];
+
+        /* Sensitivity */
+        $this->private = ($message->getSensitivity() == 'private' || $message->getSensitivity() == 'confidential') ? true :  false;
+
+        /* Response Status */
+        $status = $message->getResponseType();
+        switch ($status) {
+        case 'declined':
+            $status = 'CANCELLED';
+            break;
+        case 'accepted':
+            $status = 'CONFIRMED';
+            break;
+        case 'tenative':
+            $status = 'TENATIVE';
+        default:
+            $status = 'FREE';
+        }
+        $this->status = constant('Kronolith::STATUS_' . $status);
+
+        /* Alarm */
+        if ($alarm = $message->getReminder()) {
+            $this->alarm = $alarm;
+        }
+
+        /* Attendees */
+
+        /* Recurrence */
+        if ($rrule = $message->getRecurrence()) {
+            $this->recurrence = $rrule;
+
+            /* Exceptions */
+            /* Since AS keeps exceptions as part of the original event, we need to
+             * delete all existing exceptions and re-create them. The only drawback
+             * to this is that the UIDs will change.
+             */
+            if (!empty($this->uid)) {
+                $kronolith_driver = Kronolith::getDriver(null, $this->calendar);
+                $search = new StdClass();
+                $search->start = $rrule->getRecurStart();
+                $search->end = $rrule->getRecurEnd();
+                $search->baseid = $this->uid;
+                $results = $kronolith_driver->search($search);
+                foreach ($results as $days) {
+                    foreach ($days as $exception) {
+                        $kronolith_driver->deleteEvent($exception->id);
+                    }
+                }
+            }
+
+            $erules = $message->getExceptions();
+            foreach ($erules as $rule){
+                $d = new Horde_Date($rule->getExceptionStartTime());
+                $this->recurrence->addException($d->format('Y'), $d->format('m'), $d->format('d'));
+
+                /* Readd the exception event */
+                $event = $kronolith_driver->getEvent();
+                $times = $rule->getDatetime();
+                $event->start = $times['start'];
+                $event->end = $times['end'];
+                $event->allday = $times['allday'];
+                $event->title = Horde_String::convertCharset($rule->getSubject(), 'utf-8', Horde_Nls::getCharset());
+                $event->description = Horde_String::convertCharset($rule->getBody(), 'utf-8', Horde_Nls::getCharset());
+                $event->baseid = $this->uid;
+                $event->initialized = true;
+                $event->save();
+            }
+        }
+
+        /* Flag that we are initialized */
+        $this->initialized = true;
+    }
+
+    /**
+     * Export this event as a MS ActiveSync Message
+     *
+     * @return Horde_ActiveSync_Message_Appointment
+     */
+    public function toASAppointment()
+    {
+        $message = new Horde_ActiveSync_Message_Appointment(array('logger' => $GLOBALS['injector']->getInstance('Horde_Log_Logger')));
+        $message->setSubject(Horde_String::convertCharset($this->getTitle(), Horde_Nls::getCharset(), 'utf-8'));
+        $message->setBody(Horde_String::convertCharset($this->description, Horde_Nls::getCharset(), 'utf-8'));
+        $message->setLocation(Horde_String::convertCharset($this->location, Horde_Nls::getCharset(), 'utf-8'));
+
+        /* Start and End */
+        $message->setDatetime(array('start' => $this->start,
+                                    'end' => $this->end,
+                                    'allday' => $this->isAllDay()));
+
+        /* Timezone */
+        $message->setTimezone($this->start);
+
+        /* Organizer */
+        $name = Kronolith::getUserName($this->creator);
+        $name = Horde_String::convertCharset($name, Horde_Nls::getCharset(), 'utf-8');
+        $message->setOrganizer(
+                array('name' => $name,
+                      'email' => Kronolith::getUserEmail($this->creator))
+        );
+
+        /* Privacy */
+        $message->setSensitivity($this->private ? 'private' : 'normal');
+
+        /* Response Status */
+        switch ($this->status) {
+        case Kronolith::STATUS_CANCELLED:
+            $status = 'declined';
+            break;
+        case Kronolith::STATUS_CONFIRMED:
+            $status = 'accepted';
+            break;
+        case Kronolith::STATUS_TENTATIVE:
+            $status = 'tenative';
+        case Kronolith::STATUS_FREE:
+        case Kronolith::STATUS_NONE:
+            $status = 'none';
+        }
+        $message->setResponseType($status);
+
+        /* DTStamp */
+        $message->setDTStamp($_SERVER['REQUEST_TIME']);
+
+        /* Recurrence */
+        if ($this->recurs()) {
+            $message->setRecurrence($this->recurrence);
+
+            /* Exceptions */
+            if (!empty($this->recurrence) && $exceptions = $this->recurrence->getExceptions()) {
+                foreach ($exceptions as $start) {
+                    $e = new Horde_ActiveSync_Message_Exception();
+                    $e->setDateTime(array('start' => new Horde_Date($start)));
+                    $message->addException($e);
+                }
+            }
+        }
+
+        /* Attendees */
+
+        $message->setReminder($this->alarm);
+
+        return $message;
+    }
+
+    /**
      * Imports the values for this event from an array of values.
      *
      * @param array $hash  Array containing all the values.
index db7add1..c8b339b 100644 (file)
@@ -137,6 +137,10 @@ class Kronolith_Event_Sql extends Kronolith_Event
         if (isset($SQLEvent['event_alarm_methods'])) {
             $this->methods = $driver->convertFromDriver(unserialize($SQLEvent['event_alarm_methods']));
         }
+        if (isset($SQLEvent['event_baseid'])) {
+            $this->baseid = $SQLEvent['event_baseid'];
+        }
+
         $this->initialized = true;
         $this->stored = true;
     }
@@ -210,6 +214,10 @@ class Kronolith_Event_Sql extends Kronolith_Event
             }
             $this->_properties['event_exceptions'] = implode(',', $this->recurrence->getExceptions());
         }
+
+        if (!empty($this->baseid)) {
+            $this->_properties['event_baseid'] = $this->baseid;
+        }
     }
 
     public function getProperties()
index 937e2dd..9170948 100644 (file)
@@ -454,13 +454,16 @@ class Kronolith
      *                                 Defaults to false
      * @param boolean $showRemote      Return events from remote and
      *                                 listTimeObjects as well?
+     * @param boolean $hideExceptions  Hide events that represent exceptions to
+     *                                 a recurring event?
      *
      * @return array  The events happening in this time period.
      * @throws Kronolith_Exception
      */
     public static function listEvents($startDate, $endDate, $calendars = null,
                                       $showRecurrence = true,
-                                      $alarmsOnly = false, $showRemote = true)
+                                      $alarmsOnly = false, $showRemote = true,
+                                      $hideExceptions = false)
     {
         $results = array();
 
@@ -471,7 +474,10 @@ class Kronolith
         $driver = self::getDriver();
         foreach ($calendars as $calendar) {
             $driver->open($calendar);
-            $events = $driver->listEvents($startDate, $endDate, $showRecurrence);
+            $events = $driver->listEvents($startDate, $endDate, $showRecurrence,
+                                          $alarmsOnly, false, true,
+                                          $hideExceptions);
+            
             self::mergeEvents($results, $events);
         }
 
index 83a5c33..06b59bd 100644 (file)
@@ -23,6 +23,7 @@ CREATE TABLE kronolith_events (
     event_alarm_methods VARCHAR(MAX),
     event_modified INT NOT NULL,
     event_private INT DEFAULT 0 NOT NULL,
+    event_baseid VARCHAR(255) DEFAULT '',
 
     PRIMARY KEY (event_id)
 );
index 8055d59..70ca897 100644 (file)
@@ -23,6 +23,7 @@ CREATE TABLE kronolith_events (
     event_alarm_methods TEXT,
     event_modified INT NOT NULL,
     event_private TINYINT DEFAULT 0 NOT NULL,
+    event_baseid VARCHAR(255) DEFAULT '',
 
     PRIMARY KEY (event_id)
 );
index 3c5c4a6..c904ba9 100644 (file)
@@ -23,6 +23,8 @@ CREATE TABLE kronolith_events (
     event_alarm_methods VARCHAR2(4000),
     event_modified NUMBER(16) NOT NULL,
     event_private NUMBER(1) DEFAULT 0 NOT NULL,
+    event_baseid VARCHAR2(255) DEFAULT '',
+
 --
     PRIMARY KEY (event_id)
 );
index 130347d..2daa72a 100644 (file)
@@ -23,6 +23,7 @@ CREATE TABLE kronolith_events (
     event_alarm_methods TEXT,
     event_modified INT NOT NULL,
     event_private INT DEFAULT 0 NOT NULL,
+    event_baseid VARCHAR(255) DEFAULT '',
 
     PRIMARY KEY (event_id)
 );
index 23c3c68..d76c10f 100644 (file)
@@ -23,6 +23,7 @@ CREATE TABLE kronolith_events (
     event_alarm_methods TEXT,
     event_modified INT NOT NULL,
     event_private INT DEFAULT 0 NOT NULL,
+    event_baseid VARCHAR(255) DEFAULT '',
 
     PRIMARY KEY (event_id)
 );
index 7de2411..f402a9f 100644 (file)
     <default>0</default>
    </field>
 
+   <field>
+    <name>event_baseid</name>
+    <type>text</type>
+    <length>255</length>
+   </field>
+
    <index>
     <name>kronolith_primary</name>
     <primary>true</primary>
diff --git a/kronolith/scripts/upgrades/2010-03-25_add_baseid.sql b/kronolith/scripts/upgrades/2010-03-25_add_baseid.sql
new file mode 100644 (file)
index 0000000..831fd06
--- /dev/null
@@ -0,0 +1 @@
+ALTER TABLE kronolith_events ADD event_baseid VARCHAR(255) DEFAULT '';
index 2364228..c990027 100644 (file)
@@ -727,7 +727,8 @@ class Turba_Api extends Horde_Registry_Api
      *                               text/vcard - text/x-vcard The first two
      *                               produce a vcard3.0 (rfc2426), the second
      *                               produces a vcard in old 2.1 format
-     *                               defined by imc.org
+     *                               defined by imc.org Also supports a raw
+     *                               array
      * @param string|array $sources The source(s) from which the contact will
      *                               be exported.
      * @param array $fields          Hash of field names and SyncML_Property
@@ -780,6 +781,7 @@ class Turba_Api extends Horde_Registry_Api
                 throw new Horde_Exception("Internal Horde Error: multiple turba objects with same objectId.");
             }
 
+
             $version = '3.0';
             list($contentType,) = explode(';', $contentType);
             switch ($contentType) {
@@ -797,6 +799,16 @@ class Turba_Api extends Horde_Registry_Api
                     $export .= $vcard->exportvCalendar();
                 }
                 return $export;
+            
+            case 'array':
+                $attributes = array();
+                foreach ($result->objects as $object) {
+                    foreach ($cfgSources[$source]['map'] as $field => $map) {
+                        $attributes[$field] = $object->getValue($field);
+                    }
+                }
+
+                return $attributes;
             }
 
             throw new Horde_Exception(sprintf(_("Unsupported Content-Type: %s"), $contentType));