Skip to content

fix(activesync): stabilize folder UIDs after hierarchy changes#81

Merged
TDannhauer merged 4 commits into
FRAMEWORK_6_0from
fix/deterministic-folder-uids-moveitems-gone
Jul 9, 2026
Merged

fix(activesync): stabilize folder UIDs after hierarchy changes#81
TDannhauer merged 4 commits into
FRAMEWORK_6_0from
fix/deterministic-folder-uids-moveitems-gone

Conversation

@TDannhauer

Copy link
Copy Markdown
Contributor

Summary

  • Generate new EAS folder UIDs deterministically from backend ids (crc32) instead of mt_rand(), so concurrent FolderSync resets after hierarchy changes (e.g. moving contacts between Turba address books) assign stable UIDs
  • Return MoveItems STATUS_INVALID_SRC when source or destination folder UIDs are no longer in cache, instead of bubbling FolderGone as HTTP 500
  • Add unit tests for deterministic UID generation and MoveItems stale-folder handling

Motivation

When the folder hierarchy changes on the server, iOS devices can issue concurrent FolderSync resets and MoveItems against stale folder UIDs. Random per-reset UIDs prevented the device from converging; uncaught FolderGone during MoveItems init made recovery worse by returning HTTP 500.

Changes

  • Horde_ActiveSync_Driver_Base::_getFolderUidForBackendId(): deterministic UID via sprintf('%s%08x', $prefix, crc32($prefix . ':' . $id))
  • Horde_ActiveSync_Request_MoveItems: wrap Importer::init() in existing exception handling; catch Horde_ActiveSync_Exception_FolderGone
  • New tests: FolderUidDeterministicTest, MoveItemsFolderGoneTest

Test plan

  • vendor/bin/phpunit --bootstrap vendor/autoload.php test/unit/Horde/ActiveSync/FolderUidDeterministicTest.php
  • vendor/bin/phpunit --bootstrap vendor/autoload.php test/unit/Horde/ActiveSync/MoveItemsFolderGoneTest.php
  • Move contacts between synced and non-synced Turba address books; verify iPhone ActiveSync mail/contacts recover without account removal
  • Confirm stale MoveItems no longer produces HTTP 500 in server logs

Derive new EAS folder UIDs deterministically from backend ids so
concurrent FolderSync resets produce consistent mappings when contact
address books change. Handle stale folder references in MoveItems with
INVALID_SRC instead of HTTP 500.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This pull request improves ActiveSync resilience during folder hierarchy churn by making EAS folder UIDs deterministic (instead of random) and ensuring MoveItems returns a protocol-level error when folder UIDs are stale, preventing HTTP 500s and helping clients converge after FolderSync resets.

Changes:

  • Generate deterministic folder UIDs in Horde_ActiveSync_Driver_Base::_getFolderUidForBackendId() using a crc32-based derivation.
  • Catch Horde_ActiveSync_Exception_FolderGone during MoveItems processing and return STATUS_INVALID_SRC instead of failing the request.
  • Add unit tests covering deterministic UID generation and stale-folder MoveItems handling.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

File Description
lib/Horde/ActiveSync/Driver/Base.php Switches folder UID generation from random to deterministic based on backend id.
lib/Horde/ActiveSync/Request/MoveItems.php Ensures MoveItems handles stale folder UIDs by catching FolderGone and returning an error status.
test/unit/Horde/ActiveSync/FolderUidDeterministicTest.php Adds tests validating deterministic UID generation and persisted-map behavior.
test/unit/Horde/ActiveSync/MoveItemsFolderGoneTest.php Adds tests validating MoveItems behavior when source/destination folder UIDs are stale.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lib/Horde/ActiveSync/Driver/Base.php Outdated
Comment thread test/unit/Horde/ActiveSync/FolderUidDeterministicTest.php Outdated
Comment thread test/unit/Horde/ActiveSync/FolderUidDeterministicTest.php Outdated
@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown

🔍 CI Results

Overall: ❌ 12/12 lanes failed

TL;DR: ❌ Quality issues: PHPUnit: 54 tests failed; PHPStan: 331 unique errors in 8 lanes; PHP-CS-Fixer: 19 files.

Summary by PHP Version

PHP dev stable
8.0
8.1
8.2
8.3
8.4
8.5

Quality Metrics

  • PHPUnit: 16 failures, 38 errors out of 210 tests in 8 lanes ❌
    Per-test detail in the table below. Raw JUnit XML and JSON are uploaded as workflow artifacts.

    Which tests failed
    Type Test Lanes Message
    ❌ failure Horde\ActiveSync\AppointmentTest::testSetRecurrenceEas16SetsFirstDayOfWeek php8.2-dev, php8.2-stable, php8.3-dev, php8.3-stable, php8.4-dev, php8.4-stable, php8.5-dev, php8.5-stable [PHPUnit\Framework\ExpectationFailedException] Failed asserting that '' matches expected 0.
    ⚠️ error Horde\ActiveSync\FolderUidDeterministicTest::testFolderUidIsDeterministicAcrossDriverInstances php8.3-dev, php8.3-stable, php8.4-dev, php8.4-stable, php8.5-dev, php8.5-stable [Error] Call to undefined method PHPUnit\Framework\MockObject\MockBuilder::getMockForAbstractClass()
    ⚠️ error Horde\ActiveSync\FolderUidDeterministicTest::testFolderUidIsDeterministicForRepeatedLookups php8.3-dev, php8.3-stable, php8.4-dev, php8.4-stable, php8.5-dev, php8.5-stable [Error] Call to undefined method PHPUnit\Framework\MockObject\MockBuilder::getMockForAbstractClass()
    ⚠️ error Horde\ActiveSync\FolderUidDeterministicTest::testFolderUidUsesPersistedMapWhenPresent php8.3-dev, php8.3-stable, php8.4-dev, php8.4-stable, php8.5-dev, php8.5-stable [Error] Call to undefined method PHPUnit\Framework\MockObject\MockBuilder::getMockForAbstractClass()
    ⚠️ error Horde\ActiveSync\Request\FindRequestTest::testMockDriverReturnsSuccessfulEmptyFindResults php8.2-dev, php8.2-stable, php8.3-dev, php8.3-stable, php8.4-dev, php8.4-stable, php8.5-dev, php8.5-stable [InvalidArgumentException] Missing required state object
    ❌ failure Horde\ActiveSync\Request\FindRequestTest::testParseFindRangeHonorsClientWindow php8.2-dev, php8.2-stable, php8.3-dev, php8.3-stable, php8.4-dev, php8.4-stable, php8.5-dev, php8.5-stable [PHPUnit\Framework\ExpectationFailedException] Failed asserting that 100 is identical to 101.
    ⚠️ error Horde\ActiveSync\SyncTimeBudgetTest::testGetSyncConfigDefaultsToEmptyArray php8.3-dev, php8.3-stable, php8.4-dev, php8.4-stable, php8.5-dev, php8.5-stable [Error] Call to undefined method PHPUnit\Framework\MockObject\MockBuilder::getMockForAbstractClass()
    ⚠️ error Horde\ActiveSync\SyncTimeBudgetTest::testGetSyncConfigReturnsConfiguredValues php8.3-dev, php8.3-stable, php8.4-dev, php8.4-stable, php8.5-dev, php8.5-stable [Error] Call to undefined method PHPUnit\Framework\MockObject\MockBuilder::getMockForAbstractClass()
  • PHPStan (advisory): 331 unique errors in 8 lanes ⚠️

  • PHP-CS-Fixer: 19 unique files with issues (of 199 checked) ⚠️

❌ Failed Lanes

php8.0-dev

  • PHPUnit: ❌ Setup failed (No result file found)
  • PHPStan: ❌ Setup failed (No result file found)

php8.0-stable

  • PHPUnit: ❌ Setup failed (Composer install failed in /tmp/horde-ci/lanes/php8.0-stable/ActiveSync: Your requirements could not be resolved to an installable set of packages.)
  • PHPStan: ❌ Setup failed (Composer install failed in /tmp/horde-ci/lanes/php8.0-stable/ActiveSync: Your requirements could not be resolved to an installable set of packages.)

php8.1-dev

  • PHPUnit: ❌ Setup failed (No result file found)
  • PHPStan: ❌ Setup failed (No result file found)

php8.1-stable

  • PHPUnit: ❌ Setup failed (No result file found)
  • PHPStan: ❌ Setup failed (No result file found)

php8.2-dev

  • PHPUnit: 2 failures, 1 error

php8.2-stable

  • PHPUnit: 2 failures, 1 error

php8.3-dev

  • PHPUnit: 2 failures, 6 errors

php8.3-stable

  • PHPUnit: 2 failures, 6 errors

php8.4-dev

  • PHPUnit: 2 failures, 6 errors
  • PHP-CS-Fixer: 19 files with issues

php8.4-stable

  • PHPUnit: 2 failures, 6 errors

php8.5-dev

  • PHPUnit: 2 failures, 6 errors

php8.5-stable

  • PHPUnit: 2 failures, 6 errors

CI powered by horde-componentsView full results

TDannhauer and others added 3 commits July 9, 2026 23:17
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@TDannhauer
TDannhauer merged commit b1022a5 into FRAMEWORK_6_0 Jul 9, 2026
1 check failed
TDannhauer added a commit that referenced this pull request Jul 9, 2026
Add a focused backlog entry for persisting or atomically rebuilding
the backend-id to EAS-folder-UID map after #81. Note that opaque
UID assignment should return once the structural fix removes the
empty-map race.
@TDannhauer
TDannhauer deleted the fix/deterministic-folder-uids-moveitems-gone branch July 10, 2026 11:02
ralflang added a commit that referenced this pull request Jul 18, 2026
Release version 3.1.0

fix(sync): emit SYNC_MODIFY replies for EAS 16 email modifies
fix(activesync): throttle deferred-import keep-alive tokens
fix(activesync): address streaming review findings
docs(activesync): restructure documentation by audience
feat(activesync): defer up-sync imports and emit WBXML keep-alives when streaming
docs(activesync): note batch-size vector consolidation for Horde 6
feat(activesync): stream Sync responses incrementally
Merge pull request #82 from horde/fix/settings-oof-empty-msgs
fix(activesync): guard empty OOF messages in Settings response
docs(todo): document folder UID map long-term work
docs(activesync): explain crc32 mask in folder UID generation
Merge pull request #81 from horde/fix/deterministic-folder-uids-moveitems-gone
Potential fix for pull request finding
Potential fix for pull request finding
Potential fix for pull request finding
fix(activesync): stabilize folder UIDs after hierarchy changes
Merge pull request #80 from horde/fix/imap-mimepart-null-charset
fix(activesync): guard null charset before strcasecmp in getMimePart()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants