# Провайдер Exchange (EWS)

Документ описывает архитектуру, аутентификацию и режимы синхронизации календарей с Exchange (через протокол EWS). Рассматриваются операции с событиями, push-синхронизация, работа с вложениями, ограничения и сценарии диагностики. Руководство — для администраторов и инженеров, настраивающих интеграцию календарей с Exchange.

> **Не входит в 1F Certificate Edition (ФСТЭК-сборка).** Интеграция с Microsoft Exchange в сертифицируемую сборку не включается: из неё исключены и клиентская библиотека EWS, и модуль синхронизации с Exchange. На такой площадке недоступны календарный и почтовый провайдеры Exchange, синхронизация отсутствий и push-подписки на изменения. Работа с календарями ведётся через CalDAV, с почтой — по IMAP. В полной сборке интеграция работает без изменений.

## 1. Архитектура

Провайдер Exchange (EWS) отвечает за чтение и синхронизацию календарных событий через протокол Exchange Web Services. Идентификатор провайдера в системе — `ews`.

**Окно запроса занятости.** Доступность у Exchange запрашивается целыми сутками: берутся текущие сутки запрашивающей стороны и расширяются так, чтобы накрыть рабочие сутки каждого участника в часовом поясе его профиля. Кратность суткам обязательна — EWS сериализует окно только датами, внутрисуточная часть границы до сервера не доходит; без расширения края рабочих суток участника из другого пояса обрезались бы, и свободные интервалы расходились бы с тем, что показывает Outlook. Участник, для которого рабочее окно вычислить не удалось, пропускается, окно при этом не меняется.

**Кэши и время их жизни**

Чтобы снизить нагрузку на Exchange, провайдер кэширует часть данных. Время жизни кэша влияет на то, как быстро применяются изменения:

| Что кэшируется | Время жизни | Назначение |
|---|---|---|
| Счётчик непрочитанных приглашений | 10 мин | Бейдж непрочитанных на вкладке «События» |
| ID папки «Календарь» пользователя | до перезапуска | Поиск событий |
| ID папки «Входящие» пользователя | до перезапуска | Поиск приглашений |
| Права на просмотр чужих календарей | 5 мин (`ExchangePermissionsCacheLifeTime`) | Проверка доступа |
| Права, которые не удалось прочитать | 1 мин (`ExchangePermissionsFailedCacheLifeTime`) | Отказ в доступе не «замораживается» на общее время жизни |
| Категории и цвета событий | до перезапуска | Цветовая разметка (24 цвета) |

**Холодный кэш и ошибочные права.** Права на пару «пользователь — владелец — папка» читаются у Exchange один раз: после успешного чтения значение ставится в очередь на перечитывание и сохранение — права ложатся в `EwsFolderPermissions`, и остальные ноды берут их оттуда, а не читают ту же пару заново. Пока права по паре ещё не прочитаны, первое обращение читает их у Exchange синхронно, но результат больше не остаётся только в памяти одной ноды. Если чтение прав упало из-за сбоя Exchange, пустое значение кладётся в кэш на отдельный короткий срок (`ExchangePermissionsFailedCacheLifeTime`, по умолчанию 1 мин): отказ в доступе живёт минуту, после чего права перечитываются, а не держится на общем времени жизни кэша прав.

## 2. Аутентификация

**Метод**

Используется только **Basic Auth** — логин, пароль и домен системной учётной записи. OAuth не поддерживается (это ограничение для Exchange Online / Microsoft 365).

**Перевоплощение (impersonation)**

Соединение перевоплощается в нужного пользователя одним из двух способов:

- по **SMTP-адресу** — по умолчанию;
- по **SID** — при включённой настройке «Использовать SID для перевоплощения» (`UseSIDForImpersonalization = true`); значение SID берётся из Active Directory.

Приоритет выбора сервиса: сначала по контроллеру домена пользователя (`DomainController`), затем — сервис EWS по умолчанию.

**Версия протокола**

- `Exchange2010_SP1` — по умолчанию;
- `Exchange2013_SP1` — для прямого чтения.

> Настройка `ExchangeVersion` в CustomSettings переопределяет оба значения по умолчанию.

**Шифрование паролей**

Пароль системной учётной записи хранится в зашифрованном виде.

**Режим удаления событий**

- без перевоплощения — событие удаляется безвозвратно (`HardDelete`);
- с перевоплощением — переносится в «Удалённые» (`MoveToDeletedItems`).

## 3. Режимы синхронизации

Три режима (подробности — в [Exchange — диагностика синхронизации пользователя](https://help.1forma.ru/domains/calendar/runbook-exchange-user-sync.md)):

| Режим | `DoSyncWithExchange` | `CalendarEwsDirectSync` | Описание |
|-------|---------------------|------------------------|----------|
| **Disabled** | false | false | Нет синхронизации |
| **Sync** (событийный) | true | false | Событийная синхронизация |
| **Direct / Online** | false | true | Прямое чтение из Exchange |

**Автоотключение при ошибках**

При ошибках Exchange синхронизация пользователя автоматически отключается:

- ошибки недоступного ящика или адреса: `ErrorNonExistentMailbox`, `ErrorItemNotFound`, `ErrorFolderNotFound`, `ErrorInvalidSmtpAddress`, `ErrorMailRecipientNotFound`;
- при ошибке `AccessDenied` сбрасываются сохранённые права на папки (`EwsFolders` / `EwsFolderPermissions`) и увеличивается счётчик неудачных попыток;
- при превышении лимита `CalendarExchangeRetryLimit` режим переключается в Disabled;
- фоновая задача `EnableDoSyncWithExchangeJob` может включить синхронизацию обратно.

## 4. Создание, изменение и удаление событий

**Создание**

При создании встречи в Exchange:

- если у встречи есть участники — им рассылаются приглашения, копия сохраняется в календаре организатора; если участников нет — приглашения не отправляются;
- при включённом перевоплощении встреча создаётся в папке «Календарь» пользователя, иначе — в папке, определённой правами доступа.

**Изменение**

Существующая встреча перечитывается, обновляется и сохраняется с приоритетом значений из 1Ф. Рассылка обновлений или отмен участникам зависит от наличия участников.

**Изменение повторяющейся серии с даты (область «с этой даты и далее»).**

Отдельного маршрута и отдельного контракта у Exchange нет: используется тот же `PUT api/calendar/update`, что и для остальных правок, с необязательными полями `seriesScope` (`All` | `ThisAndFollowing`) и `seriesFrom`. Провайдер выбирается по префиксу ключа `ews:`. Порядок операций:

1. **Кто может.** Разрезать серию может только организатор: у кликнувшего вхождение сверяется признак организатора, у участника разрез отклоняется (`CalendarAccessDeniedException`).
2. **Поиск мастера.** Кликнутое вхождение или отдельно изменённое повторение привязывается к родительской встрече серии; одиночное событие разрезать нельзя (`Cannot split non-recurring event`).
3. **Проверка границы.** Дата раньше начала серии или позже её конца отклоняется до записи, с текстом `SeriesFrom date is before the beginning of the series` / `SeriesFrom date is after the end of the series`.
4. **Обрезка прежней серии.** Мастеру ставится дата окончания днём перед границей. Если граница совпала с началом серии, прежняя серия отменяется (у встреч с участниками) или удаляется — вхождений у неё не остаётся.
5. **Создание новой серии.** Создаётся с полями формы и правилом повторения от даты границы. Если правило повторения задано формой, берётся оно, иначе копируется прежнее с новой датой старта. Если у серии было ограничение «число повторений», новой достаётся остаток после вхождений, оставшихся в прежней.
6. **Перенос индивидуальных правок.** Повторения, изменённые отдельно от серии, снимаются снимками до обрезки и переносятся в новую серию вместе со своими полями и составом участников; повторения, снятые отдельно, удаляются в новой серии. Поиск нужного вхождения идёт по исходной дате, снятые вхождения EWS держат свои индексы и корректно пропускаются.
7. **Письма.** Сохранение новой серии и перенос правок идут без уведомлений; участники получают два письма — об изменении прежней серии (с обрезкой) и приглашение на новую, каждое по одному разу.
8. **Ответы участников.** Ответы на новую серию **не переносятся**: снимок отдельно изменённого повторения не несёт признак ответа, поэтому участники получают приглашение на новую серию и отвечают заново — так же, как при обычном приглашении в Outlook. Это осознанное решение: у встроенного календаря 1Формы ответы при разрезе сохраняются, у Exchange — нет.

Если новая серия не создалась, прежняя дата окончания (или число повторений) восстанавливается без уведомлений. Исключение — случай, когда граница совпала с началом серии: прежняя серия к этому моменту уже отменена или удалена, вернуть её EWS не позволяет, поэтому в журнал пишется ошибка с ключом события и идентификаторами пользователей, а серию нужно пересоздать вручную.


**Удаление**

Встреча удаляется в режиме, определённом настройкой перевоплощения (см. §2 «Режим удаления событий»).

**Чтение и уровни доступа**

Объём данных встречи, который видит пользователь, зависит от его прав на папку. Права выражены собственными перечислениями, развязанными от типов библиотеки EWS: уровень чтения — `CalendarFolderReadAccess`, права на изменение и удаление — `CalendarPermissionScope` (`None`, `Owned`, `All`), причём изменение и удаление встречи выполняются только при значении `All`. Числовые значения обоих перечислений совпадают с исходными EWS-типами (`FolderPermissionReadAccess` и `PermissionScope`), поэтому прочитанные из Exchange права приводятся к ним прямым приведением типа, а накопленные значения в `EwsFolderPermissions` читаются без преобразования.

| Уровень доступа | Значение `CalendarFolderReadAccess` | Что видно |
|---|---|---|
| Нет доступа | `None` | Ничего: события папки не читаются |
| Только время | `TimeOnly` | Время начала и окончания, статус занятости |
| Время, тема, место | `TimeAndSubjectAndLocation` | + тема и место встречи |
| Полный доступ | `FullDetails` | Всё, включая тело, участников и вложения |

Приватные события (`Sensitivity.Private`) скрыты от других пользователей. Для повторяющихся событий отдельные экземпляры и исключения читаются через родительскую встречу серии.

**Маппинг полей**

Соответствие полей события 1Ф и Exchange при создании и изменении:

| Поле 1Forma | Поле Exchange | Примечание |
|-------------|--------------|------------|
| Subject | Subject | |
| Location | Location | |
| TextBody | Body (HTML) | Несмотря на имя «TextBody», в Exchange передаётся HTML с блоком `forma-id-{taskId}` |
| Start/End | Start/End | С коррекцией таймзон |
| IsAllDayEvent | IsAllDayEvent | |
| IsPrivate | Sensitivity | Private / Normal |
| FreeBusyStatus | LegacyFreeBusyStatus | Free / Busy / Tentative / OOF |
| RequiredAttendees | RequiredAttendees | Обязательные участники |
| OptionalAttendees | OptionalAttendees | Необязательные участники |
| Recurrence | Recurrence | Повторяемость |
| Attachments | Attachments | Вложения |
| ReminderMinutes | IsReminderSet + ReminderMinutesBeforeStart | Число минут включает напоминание и записывает задержку до начала, пустое значение выключает его. При чтении значение восстанавливается из этой пары. Хранит Exchange |

**Связь события с задачей (LinkedTaskId)**

Тело встречи формируется почтовым шаблоном `MeetingCreatedWithLinkedTask`: в него передаётся параметр `taskidmarker` с машинным маркером связи `forma-id-{taskId}` (HTML-тег `span` с классом `forma-id-{идентификатор}`), который встраивается в тело события. Метод `GetLinkedTaskId` считывает из тела идентификатор задачи и заполняет поле **«Связано с»** на карточке события, а также обеспечивает работу смарт-действия **«Найти встречу по задаче»** (`FindMeetingByTaskId`).

Для событий, созданных из задачи, связь дополнительно хранится в служебном свойстве события `ValhallaTaskId` и в списках событий читается именно из него, а разбор текста остаётся для карточки события. Событие, созданное в календаре 1Формы, это свойство не получает, поэтому в отборе встреч по связи с задачей оно не участвует.

Поддерживаются формат `forma-id-(\d+)` и устаревший `Linked task id: \d+`, поэтому связь корректно извлекается и для ранее созданных встреч, и для кастомных шаблонов.

**Письмо-уведомление участникам при создании встречи**

При создании встречи в Exchange 1Форма отправляет участникам письмо по шаблону:

| Шаблон | ID события (`TCEvents`) | Условие | Тело письма |
|--------|--------------|---------|-------------|
| `MeetingCreated` | 54 | встреча не связана с задачей | Текст пользователя |
| `MeetingCreatedWithLinkedTask` | 55 | встреча связана с задачей | Текст + блок «Перейти к задаче» |

Шаблоны настраиваются в **Администрирование → Почта → Шаблоны писем**. Поставляются как системные (`SystemTemplate = 1`, `IsActive = 1`).

## 5. Push-синхронизация (Streaming Subscriptions)

**Где работает**

Push-подписки на изменения в Exchange работают **только на сервере заданий (JobServer)**. Если JobServer не запущен — push-уведомлений не будет (события подтянутся только при следующем чтении календаря).

### Как определяется список EWS-сервисов для подписки

Перед подпиской пользователей система собирает список используемых EWS-сервисов двумя путями.

**Путь 1 — по доменам AD-профилей.** Если EWS-сервис связан с профилем синхронизации Active Directory, он определяется по контроллеру домена (`DomainController`) из AD-учётных данных профиля. Если ни один EWS-сервис не привязан к AD-профилю — этот путь не даёт ни одного сервиса.

**Путь 2 — резервный, через `Settings.DefaultEwsServiceId`.** Если в таблице `Settings` поле `DefaultEwsServiceId` пустое (`NULL`) — резервный сервис не добавляется.

**Сценарий «тихого отказа»**

Если оба пути вернули 0 сервисов — список пуст, подписки не создаются и **не активируются**. При этом в журнал ничего не пишется (нет записей в `AutomationScriptsLog`, `Type = 10`). Симптом: событие в Outlook создано, но комментарий в задачу не приходит, а лог пуст.

**Диагностика:**

```sql
-- 1. Проверить DefaultEwsServiceId
SELECT DefaultEwsServiceId FROM Settings;

-- 2. Проверить, что EWS-сервисы вообще заведены
SELECT Id, ServiceId, EwsUrl, EwsLogin FROM EwsServiceSettings;

-- 3. Проверить привязку AD-профилей к EWS (наполнение кэша доменов)
SELECT sp.Id, sp.IsActive, sp.ServiceId, ads.Id AS ADSettingsId
FROM SynchronizationProfiles sp
LEFT JOIN SynchronizationProfilesADSettings ads ON ads.SynchronizationProfileId = sp.Id;
```

Если `DefaultEwsServiceId = NULL` и AD-профилей нет — подписки не стартуют. Исправление:

```sql
UPDATE Settings SET DefaultEwsServiceId = <Id>  -- Id из EwsServiceSettings
```

После обновления требуется перезапуск приложения.

### Подписки и принципы работы

Потоковые подписки на изменения работают по следующим принципам:

1. При старте система подписывается на всех неуволенных пользователей с включённой синхронизацией (`CalendarEwsDirectSync` или `DoSyncWithExchange`).
2. На каждый почтовый адрес создаётся отдельная подписка на потоковые уведомления.
3. Отслеживаемые события календаря: `Created`, `Deleted`, `Modified`, `Moved`, `Copied`, `FreeBusyChanged`.
4. События папки «Входящие»: **только `NewMail`** (новое письмо).
5. Время жизни потокового соединения — `ExchangeConnectionLifetime` (по умолчанию 1 мин).
6. При разрыве соединение восстанавливается автоматически.

**Как уведомление доходит до пользователя**

Exchange присылает уведомление об изменении события. 1Форма определяет роль пользователя (организатор или участник) и тип изменения, синхронизирует событие в задачу 1Ф и отправляет пользователю push-уведомление (SignalR) — браузер перечитывает агенду. Параллельно об изменении узнают внутренние подписчики системы.

**Параллельность**

Подписки обрабатываются параллельно; число одновременно обрабатываемых задаётся настройкой `ExchangeSemaphoreCount` (по умолчанию 50).

### Как обрабатывается изменение события

Полученное от Exchange уведомление обрабатывается по шагам:

1. Уведомления фильтруются (события уровня папки отбрасываются).
2. Группируются по событию (перемещение порождает несколько уведомлений).
3. Определяется главный тип изменения по приоритету: перемещение → изменение → создание → удаление.
4. Загружается минимальный набор полей события для проверки применимости.
5. Проверки: является ли событие мастером серии, не входит ли в исключённые категории, не слишком ли оно старое.
6. Дальнейшая обработка зависит от роли пользователя:

| Роль | Создание | Изменение | Перемещение |
|------|----------|-----------|-------------|
| **Организатор** | событие создаётся в 1Ф | событие обновляется в 1Ф | создание или отмена (зависит от направления переноса) |
| **Участник** | событие создаётся в 1Ф | событие обновляется в 1Ф | отметка об отклонении встречи |

### Что НЕ покрывают push-подписки

Часть изменений push-подписки не доставляют в реальном времени:

| Ограничение | Описание |
|-----|----------|
| **Нет push «новое приглашение»** | Подписка на «Входящие» ловит только новое письмо (`NewMail`). Приглашение на встречу в «Входящих» не порождает отдельного календарного уведомления. Пользователь узнаёт о приглашении только при открытии вкладки «События» или из почтового клиента |
| **FreeBusyChanged** | Подписка есть, обработчик не задействован — изменение, по-видимому, игнорируется |
| **Copied** | Подписка есть, отдельной обработки нет |
| **Ответы (принятие/отклонение)** | Организатор видит обновлённый статус в календаре, но отдельного уведомления «пользователь X ответил» нет |

## 6. Приглашения на встречи (вкладка «События»)

**Список приглашений**

Чтение приглашений из папки «Входящие» работает только при двух условиях:

- включён доступ к папке «Входящие» (`CalendarInboxAccessEnabled = true`);
- пользователь запрашивает свои собственные приглашения (чужие «Входящие» недоступны).

Ищутся элементы папки «Входящие» по типу (`ItemClass`):

| ItemClass | Тип | Описание |
|-----------|-----|----------|
| `IPM.Schedule.Meeting.Request` | Приглашение | Приглашение на встречу |
| `IPM.Schedule.Meeting.Canceled` | Отмена | Отмена встречи |
| `IPM.Schedule.Meeting.Resp.*` | Ответ | Ответ участника (с предложенным временем) |

Сортировка — по дате получения, от новых к старым.

**Счётчик непрочитанных**

- значение кэшируется на 10 минут;
- учитываются непрочитанные приглашения и отмены встреч за последние 14 дней;
- область поиска — папка «Входящие».

**Сброс счётчика**

При прочтении приглашений кэш очищается, а счётчик в интерфейсе обновляется через SignalR.

**Чтение одиночного события.** Элемент запрашивается у Exchange по идентификатору без предположения о его типе, и дальше разбирается по фактическому типу из ответа. Обычная встреча (`Appointment`) читается прежним путём. Приглашение на встречу (`MeetingRequest`) собирается в событие только для чтения: с пустым набором действий, без связи с задачей и без напоминания. Элементы прочих типов не поддерживаются и дают «событие не найдено».

Приглашение в чужом календаре недоступно, если оно приватное или свойство приватности вообще не пришло, — запрос отклоняется как обращение к закрытому событию. Владелец календаря такого ограничения не встречает.

Уровень доступа к папке действует и здесь. При `TimeOnly` возвращается только интервал: вместо темы подставляется состояние занятости, место пустое. При `TimeAndSubjectAndLocation` добавляются тема и место. Полный состав — организатор, участники, описание, повторяемость, вложения, признак ответа — только при полном доступе; на урезанных уровнях признак ответа не заполняется.

## 7. Вложения и обработка HTML

**Добавление и обновление вложений**

1. Текущие вложения встречи сравниваются с переданными.
2. Отсутствующие удаляются.
3. Новые загружаются и прикрепляются к встрече.
4. Расширения проверяются на список запрещённых (настройка запрещённых расширений файлов).
5. Поддерживается прикрепление файлов из хранилища 1Формы.

**Получение вложений**

Доступно только при полном уровне доступа к встрече. Несколько вложений отдаются одним ZIP-архивом.

**Обработка HTML-тела**

Из тела встречи удаляется служебный блок-контейнер 1Ф и устаревший маркер `Linked task id: <номер>`, извлекается содержимое `<body>`.

**Очистка тела при записи.** Тело встречи, отправляемое в Exchange, проходит очистку разметки: небезопасные теги и атрибуты вырезаются, а тело в виде простого текста кодируется. Если почтовый шаблон не отрисовался, в Exchange уходит тело, приведённое к простому тексту, а не исходное.

## 8. Настройки CustomSettings

**Пул и таймауты**

| Ключ | Default | Назначение |
|------|---------|-----------|
| `ExchangeConnectionPoolSize` | 100 | Размер семафора конкурентности |
| `CalendarRequestTimeoutInMilliseconds` | (EwsRequestsTimeout) | Таймаут на запрос списка |
| Таймаут семафора | **2 сек** (фиксированное значение) | Ошибка ожидания при превышении |
| Таймаут сервиса | **30 мин** (фиксированное значение) | — |

**Streaming subscriptions**

| Ключ | Default | Назначение |
|------|---------|-----------|
| `EnableEwsSubscriptions` | true | Вкл/выкл push-подписки |
| `EnableEwsEmailSubscriptions` | true | Подписки на Inbox |
| `EnableEwsCalendarInboxAccess` | true | Доступ к папке Inbox |
| `ExchangeConnectionLifetime` | 1 мин | Время жизни потокового соединения |
| `ExchangeSemaphoreCount` | 50 | Параллельные подписки |
| `ExchangeSemaphoreWait` | 0 | Таймаут семафора подписок |

**Поведение**

| Ключ | Default | Назначение |
|------|---------|-----------|
| `ExchangePermissionsCacheLifeTime` | 5 мин | TTL кэша прав |
| `ExchangePermissionsFailedCacheLifeTime` | 1 мин | TTL кэша прав, которые не удалось прочитать из-за сбоя Exchange |
| `EnableEwsSetDirectSyncDisabledWhenEwsErrorsOccurs` | true | Автоотключение при ошибках |
| `WriteEwsRequestDurationToAutomationLog` | false | Логирование длительности |
| `ExchangeSubscriptionsToLog` | false | Подробное логирование подписок |

## 9. Уровни доступа к данным события и таймзоны

**Какие поля события читаются**

В зависимости от прав пользователя из Exchange читается разный набор полей события:

| Уровень | Когда применяется | Какие поля |
|---|---|---|
| Только время | Нет прав или включён показ только занятости | Время начала/окончания, таймзона, признак «весь день», статус занятости |
| Время + тема и место | Частичный доступ | + тема, место |
| Список (краткий) | Полный доступ, режим списка | Все поля, кроме тела и участников |
| Полный | Открытие одного события | Всё: тело, участники, повторяемость, вложения |

**Ограничение выборки:** не более 5000 событий за один запрос.

**События «весь день» и таймзоны**

Даты событий «весь день» корректируются с учётом таймзоны пользователя, создателя и Exchange-сервиса. Это частый источник ошибок в сценариях с разными часовыми поясами.

## 10. Ограничения и известные проблемы

Известные ограничения провайдера Exchange и связанные с ними проблемы:

| # | Проблема | Детали |
|---|----------|--------|
| 1 | **Нет OAuth** | Только Basic Auth и перевоплощение. Ограничение для Exchange Online / Microsoft 365 |
| 2 | **Ограничение конкурентности** | Соединения не пулируются; одновременная конкурентность ограничена (100), таймаут ожидания — 2 сек |
| 3 | **Занятость сервера Exchange** | При ошибке «сервер занят» (`ServerBusyException`) повторных попыток нет: ошибка логируется, возвращается пустой результат |
| 4 | **Медленное чтение мастера серии** | Получение родительской встречи повторяющейся серии — медленная операция; в списках отключено, выполняется только при открытии события |
| 5 | **Приглашения только для владельца** | Список приглашений доступен только для своих «Входящих»; чужие недоступны |
| 6 | **Лимит выборки 5000 событий** | Фиксированное значение; при превышении выборка обрезается |
| 7 | **Push-подписки только на JobServer** | Если сервер заданий не запущен — push-уведомлений нет |
| 8 | **Пустая вкладка «События» из-за кэша папки** | Известная ошибка: при старте приложения кэш ID папки «Входящие» может заполниться идентификатором папки «Календарь», и приглашения ищутся не в той папке → вкладка пуста. После истечения кэша (24 ч) может заработать само |
| 9 | **Нет push о новом приглашении** | Подписка на «Входящие» ловит только новое письмо. Вкладка «События» обновляется только по запросу — уведомления «вам пришло приглашение» в реальном времени нет |
| 10 | **Приглашение доступно только для чтения** | Событие, прочитанное из приглашения, приходит без действий: принять, отклонить или предложить другое время из 1Формы нельзя, ответ даётся в Exchange. Связь с задачей и напоминание у такого события не заполняются |
| 11 | **Разрез серии доступен только организатору, ответы участников не переносятся** | Правку «с этой даты и далее» выполняет организатор; у участника запрос отклоняется (`CalendarAccessDeniedException`). При разрезе новая серия — новая встреча: участники получают приглашение и отвечают заново, их прежние ответы не переносятся (для встроенного календаря 1Формы ответы сохраняются). Если создание новой серии упало при границе, совпавшей с началом прежней, прежняя серия остаётся отменённой — её нужно пересоздать вручную, в журнале будет ошибка `ExchangeCalendarProvider.UpdateThisAndFollowing` с ключом события |

## 11. Диагностика

**Типичные симптомы**

| Симптом | Где смотреть |
|---------|-------------|
| Пустая вкладка «События» | 1) Проблема №8: кэш подменяет папку «Входящие» на «Календарь». 2) Настройка `EnableEwsCalendarInboxAccess`. 3) Запрашиваются ли свои собственные приглашения. 4) Если `/api/mail/find-messages` работает — соединение с EWS в порядке, проблема в кэше |
| Нет push-обновлений | Проверить `EnableEwsSubscriptions` и что запущен сервер заданий (JobServer). Смотреть логи синхронизации Exchange |
| Ошибка ожидания семафора | Нагрузка более 100 параллельных запросов. Увеличить `ExchangeConnectionPoolSize` |
| Автоотключение синхронизации | `SyncWithExchangeFailedAttempts` превысил `CalendarExchangeRetryLimit`. Сбросить счётчик в профиле пользователя |
| Приватные события не видны | Приватные события (`Sensitivity.Private`) скрыты намеренно; зависит от уровня доступа к папке |
| После правки серии с даты у участника пропала часть встреч или пришло лишнее приглашение | Проверить, что в календаре две серии: прежняя, обрезанная днём перед границей, и новая — от границы. Одно приглашение на новую серию и одно письмо об изменении прежней — это правильное поведение. Если серия осталась одна, в журнале искать ошибку `ExchangeCalendarProvider.UpdateThisAndFollowing` (граница совпала с началом серии — серию нужно пересоздать вручную) |

**Диагностический SQL**

```sql
-- Режим синхронизации пользователя
select u.UserID, u.UserName, u.Email, u.SID,
       u.CalendarEwsDirectSync, u.DoSyncWithExchange,
       u.SyncWithExchangeFailedAttempts, u.DomainController
from Users u
where u.UserID = @userId;

-- Кэш непрочитанных сообщений
select c.[Key], c.Value, c.UpdatedDate
from UsersMessageCountCache c
where c.[Key] = cast(@userId as varchar(50));

-- EWS-права пользователя на чужие папки
select f.UserID, f.FolderOwnerUserID, f.FolderId,
       p.ReadAccess, p.PermissionLevel
from EwsFolders f
join EwsFolderPermissions p on f.ID = p.EwsFolderID
where f.UserID = @userId;
```

**Связанные документы**

- [Календарь — администрирование](https://help.1forma.ru/domains/calendar/admin.md) — пошаговая настройка
- [Exchange — диагностика синхронизации пользователя](https://help.1forma.ru/domains/calendar/runbook-exchange-user-sync.md)
- [Провайдер CalDAV](https://help.1forma.ru/domains/calendar/provider-caldav.md)
