Провайдер Exchange (EWS)¶
Документ описывает архитектуру, аутентификацию и режимы синхронизации календарей с Exchange (через протокол EWS). Рассматриваются операции с событиями, push-синхронизация, работа с вложениями, ограничения и сценарии диагностики. Руководство — для администраторов и инженеров, настраивающих интеграцию календарей с Exchange.
1. Архитектура¶
Провайдер Exchange (EWS) отвечает за чтение и синхронизацию календарных событий через протокол Exchange Web Services. Идентификатор провайдера в системе — ews.
Кэши и время их жизни
Чтобы снизить нагрузку на Exchange, провайдер кэширует часть данных. Время жизни кэша влияет на то, как быстро применяются изменения:
| Что кэшируется | Время жизни | Назначение |
|---|---|---|
| Счётчик непрочитанных приглашений | 10 мин | Бейдж непрочитанных на вкладке «События» |
| ID папки «Календарь» пользователя | до перезапуска | Поиск событий |
| ID папки «Входящие» пользователя | до перезапуска | Поиск приглашений |
| Права на просмотр чужих календарей | 5 мин (ExchangePermissionsCacheLifeTime) |
Проверка доступа |
| Категории и цвета событий | до перезапуска | Цветовая разметка (24 цвета) |
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 — диагностика синхронизации пользователя):
| Режим | 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Ф. Рассылка обновлений или отмен участникам зависит от наличия участников.
Удаление
Встреча удаляется в режиме, определённом настройкой перевоплощения (см. §2 «Режим удаления событий»).
Чтение и уровни доступа
Объём данных встречи, который видит пользователь, зависит от его прав на папку (FolderPermissionReadAccess):
| Уровень доступа | Что видно |
|---|---|
| Только время | Время начала и окончания, статус занятости |
| Время, тема, место | + тема и место встречи |
| Полный доступ | Всё, включая тело, участников и вложения |
Приватные события (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 | Вложения |
Связь события с задачей (LinkedTaskId)
С версии 2.267 блок <div class="forma-link"> в тело встречи больше не вставляется. Приглашения участникам формируются отдельным письмом (см. ниже).
Для ранее созданных встреч связь с задачей по-прежнему извлекается из тела по шаблонам 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 создано, но комментарий в задачу не приходит, а лог пуст.
Диагностика:
-- 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-профилей нет — подписки не стартуют. Исправление:
UPDATE Settings SET DefaultEwsServiceId = <Id> -- Id из EwsServiceSettings
После обновления требуется перезапуск приложения.
Подписки и принципы работы¶
Потоковые подписки на изменения работают по следующим принципам:
- При старте система подписывается на всех неуволенных пользователей с включённой синхронизацией (
CalendarEwsDirectSyncилиDoSyncWithExchange). - На каждый почтовый адрес создаётся отдельная подписка на потоковые уведомления.
- Отслеживаемые события календаря:
Created,Deleted,Modified,Moved,Copied,FreeBusyChanged. - События папки «Входящие»: только
NewMail(новое письмо). - Время жизни потокового соединения —
ExchangeConnectionLifetime(по умолчанию 1 мин). - При разрыве соединение восстанавливается автоматически.
Как уведомление доходит до пользователя
Exchange присылает уведомление об изменении события. 1Форма определяет роль пользователя (организатор или участник) и тип изменения, синхронизирует событие в задачу 1Ф и отправляет пользователю push-уведомление (SignalR) — браузер перечитывает агенду. Параллельно об изменении узнают внутренние подписчики системы.
Параллельность
Подписки обрабатываются параллельно; число одновременно обрабатываемых задаётся настройкой ExchangeSemaphoreCount (по умолчанию 50).
Как обрабатывается изменение события¶
Полученное от Exchange уведомление обрабатывается по шагам:
- Уведомления фильтруются (события уровня папки отбрасываются).
- Группируются по событию (перемещение порождает несколько уведомлений).
- Определяется главный тип изменения по приоритету: перемещение → изменение → создание → удаление.
- Загружается минимальный набор полей события для проверки применимости.
- Проверки: является ли событие мастером серии, не входит ли в исключённые категории, не слишком ли оно старое.
- Дальнейшая обработка зависит от роли пользователя:
| Роль | Создание | Изменение | Перемещение |
|---|---|---|---|
| Организатор | событие создаётся в 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.
7. Вложения и обработка HTML¶
Добавление и обновление вложений
- Текущие вложения встречи сравниваются с переданными.
- Отсутствующие удаляются.
- Новые загружаются и прикрепляются к встрече.
- Расширения проверяются на список запрещённых (настройка запрещённых расширений файлов).
- Поддерживается прикрепление файлов из хранилища 1Формы.
Получение вложений
Доступно только при полном уровне доступа к встрече. Несколько вложений отдаются одним ZIP-архивом.
Обработка HTML-тела
Из тела встречи удаляется служебный блок-контейнер 1Ф и устаревший маркер Linked task id: <номер>, извлекается содержимое <body>.
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 кэша прав |
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 о новом приглашении | Подписка на «Входящие» ловит только новое письмо. Вкладка «События» обновляется только по запросу — уведомления «вам пришло приглашение» в реальном времени нет |
11. Диагностика¶
Типичные симптомы
| Симптом | Где смотреть |
|---|---|
| Пустая вкладка «События» | 1) Проблема №8: кэш подменяет папку «Входящие» на «Календарь». 2) Настройка EnableEwsCalendarInboxAccess. 3) Запрашиваются ли свои собственные приглашения. 4) Если /api/mail/find-messages работает — соединение с EWS в порядке, проблема в кэше |
| Нет push-обновлений | Проверить EnableEwsSubscriptions и что запущен сервер заданий (JobServer). Смотреть логи синхронизации Exchange |
| Ошибка ожидания семафора | Нагрузка более 100 параллельных запросов. Увеличить ExchangeConnectionPoolSize |
| Автоотключение синхронизации | SyncWithExchangeFailedAttempts превысил CalendarExchangeRetryLimit. Сбросить счётчик в профиле пользователя |
| Приватные события не видны | Приватные события (Sensitivity.Private) скрыты намеренно; зависит от уровня доступа к папке |
Диагностический 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;
Связанные документы
- Календарь — администрирование — пошаговая настройка
- Exchange — диагностика синхронизации пользователя
- Провайдер CalDAV