mirror of
https://github.com/RocketChat/Rocket.Chat.git
synced 2025-12-28 06:47:25 +00:00
feat: support mapping users to exchange servers by domain (#36153)
This commit is contained in:
parent
8033bdb1f6
commit
3779de0e8c
7
.changeset/thirty-lemons-talk.md
Normal file
7
.changeset/thirty-lemons-talk.md
Normal file
@ -0,0 +1,7 @@
|
||||
---
|
||||
'@rocket.chat/core-typings': minor
|
||||
'@rocket.chat/i18n': minor
|
||||
'@rocket.chat/meteor': minor
|
||||
---
|
||||
|
||||
Adds a new setting to override outlook calendar settings per user email domain
|
||||
196
apps/meteor/app/api/server/helpers/getUserInfo.spec.ts
Normal file
196
apps/meteor/app/api/server/helpers/getUserInfo.spec.ts
Normal file
@ -0,0 +1,196 @@
|
||||
import { getUserInfo } from './getUserInfo';
|
||||
import type { CachedSettings } from '../../../settings/server/CachedSettings';
|
||||
|
||||
jest.mock('@rocket.chat/models', () => ({
|
||||
Users: {
|
||||
findOneById: jest.fn().mockResolvedValue({
|
||||
id: '123',
|
||||
name: 'Test User',
|
||||
emails: [{ address: 'test@example.com' }],
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
const settings = new Map<string, unknown>();
|
||||
|
||||
jest.mock('../../../settings/server', () => ({
|
||||
settings: {
|
||||
getByRegexp(_id) {
|
||||
return [...settings].filter(([key]) => key.match(_id)) as any;
|
||||
},
|
||||
get(_id) {
|
||||
return settings.get(_id) as any;
|
||||
},
|
||||
set(record) {
|
||||
settings.set(record._id, record.value);
|
||||
},
|
||||
} satisfies Partial<CachedSettings>,
|
||||
}));
|
||||
|
||||
// @ts-expect-error __meteor_runtime_config__ is not defined in the type definitions
|
||||
global.__meteor_runtime_config__ = {
|
||||
ROOT_URL: 'http://localhost:3000',
|
||||
ROOT_URL_PATH_PREFIX: '',
|
||||
};
|
||||
|
||||
describe('getUserInfo', () => {
|
||||
let user: Parameters<typeof getUserInfo>[0];
|
||||
|
||||
beforeEach(() => {
|
||||
settings.clear();
|
||||
settings.set('Site_Url', 'http://localhost:3000');
|
||||
user = {
|
||||
_id: '123',
|
||||
createdAt: new Date(),
|
||||
roles: [],
|
||||
type: 'user',
|
||||
active: true,
|
||||
_updatedAt: new Date(),
|
||||
};
|
||||
});
|
||||
|
||||
it('should return user info', async () => {
|
||||
const userInfo = await getUserInfo(user);
|
||||
|
||||
expect(userInfo).toEqual(
|
||||
expect.objectContaining({
|
||||
_id: '123',
|
||||
type: 'user',
|
||||
roles: [],
|
||||
active: true,
|
||||
_updatedAt: expect.any(Date),
|
||||
createdAt: expect.any(Date),
|
||||
email: undefined,
|
||||
avatarUrl: 'http://localhost:3000/avatar/undefined',
|
||||
settings: {
|
||||
calendar: {},
|
||||
profile: {},
|
||||
preferences: {},
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
describe('email handling', () => {
|
||||
it('should not include email if no emails are present', async () => {
|
||||
user.emails = [];
|
||||
const userInfo = await getUserInfo(user);
|
||||
expect(userInfo.email).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should include email if one email is present and verified', async () => {
|
||||
user.emails = [{ address: 'test@example.com', verified: true }];
|
||||
const userInfo = await getUserInfo(user);
|
||||
expect(userInfo.email).toEqual('test@example.com');
|
||||
});
|
||||
|
||||
it('should not include email if one email is present and not verified', async () => {
|
||||
user.emails = [{ address: 'test@example.com', verified: false }];
|
||||
const userInfo = await getUserInfo(user);
|
||||
expect(userInfo.email).toBe(undefined);
|
||||
});
|
||||
|
||||
it('should include email if multiple emails are present and one is verified', async () => {
|
||||
user.emails = [
|
||||
{ address: 'test@example.com', verified: false },
|
||||
{ address: 'test2@example.com', verified: true },
|
||||
];
|
||||
const userInfo = await getUserInfo(user);
|
||||
expect(userInfo.email).toEqual('test2@example.com');
|
||||
});
|
||||
|
||||
it('should not include email if multiple emails are present and none are verified', async () => {
|
||||
user.emails = [
|
||||
{ address: 'test@example.com', verified: false },
|
||||
{ address: 'test2@example.com', verified: false },
|
||||
];
|
||||
const userInfo = await getUserInfo(user);
|
||||
expect(userInfo.email).toBe(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
describe('outlook calendar settings', () => {
|
||||
beforeEach(() => {
|
||||
settings.set('Outlook_Calendar_Enabled', true);
|
||||
settings.set('Outlook_Calendar_Exchange_Url', 'https://outlook.office365.com/');
|
||||
settings.set('Outlook_Calendar_Outlook_Url', 'https://outlook.office365.com/owa/#calendar/view/month');
|
||||
settings.set('Outlook_Calendar_Url_Mapping', JSON.stringify({}));
|
||||
user.emails = [{ address: 'test@example.com', verified: true }];
|
||||
});
|
||||
|
||||
it('should return empty calendar settings if Outlook is disabled', async () => {
|
||||
settings.set('Outlook_Calendar_Enabled', false);
|
||||
const userInfo = await getUserInfo(user);
|
||||
expect(userInfo.settings?.calendar).toEqual({});
|
||||
});
|
||||
|
||||
it('should return calendar settings with Outlook enabled and default URLs', async () => {
|
||||
const userInfo = await getUserInfo(user);
|
||||
expect(userInfo.settings?.calendar?.outlook).toEqual({
|
||||
Enabled: true,
|
||||
Exchange_Url: 'https://outlook.office365.com/',
|
||||
Outlook_Url: 'https://outlook.office365.com/owa/#calendar/view/month',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return calendar settings with Outlook enabled and domain mapping', async () => {
|
||||
settings.set(
|
||||
'Outlook_Calendar_Url_Mapping',
|
||||
JSON.stringify({
|
||||
'example.com': { Exchange_Url: 'https://custom.exchange.com/', Outlook_Url: 'https://custom.outlook.com/' },
|
||||
}),
|
||||
);
|
||||
const userInfo = await getUserInfo(user);
|
||||
expect(userInfo.settings?.calendar).toEqual({
|
||||
outlook: {
|
||||
Enabled: true,
|
||||
Exchange_Url: 'https://custom.exchange.com/',
|
||||
Outlook_Url: 'https://custom.outlook.com/',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should return calendar settings with outlook disabled but enabled for specific domain', async () => {
|
||||
settings.set('Outlook_Calendar_Enabled', false);
|
||||
settings.set(
|
||||
'Outlook_Calendar_Url_Mapping',
|
||||
JSON.stringify({
|
||||
'specific.com': { Enabled: true, Exchange_Url: 'https://specific.exchange.com/', Outlook_Url: 'https://specific.outlook.com/' },
|
||||
}),
|
||||
);
|
||||
user.emails = [{ address: 'me@specific.com', verified: true }];
|
||||
const userInfo = await getUserInfo(user);
|
||||
expect(userInfo.settings?.calendar).toEqual({
|
||||
outlook: {
|
||||
Enabled: true,
|
||||
Exchange_Url: 'https://specific.exchange.com/',
|
||||
Outlook_Url: 'https://specific.outlook.com/',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should return calendar settings with Outlook enabled and default mapping for unknown domain', async () => {
|
||||
user.emails = [{ address: 'unknown@example.com', verified: true }];
|
||||
const userInfo = await getUserInfo(user);
|
||||
expect(userInfo.settings?.calendar).toEqual({
|
||||
outlook: {
|
||||
Enabled: true,
|
||||
Exchange_Url: 'https://outlook.office365.com/',
|
||||
Outlook_Url: 'https://outlook.office365.com/owa/#calendar/view/month',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle invalid JSON in Outlook_Calendar_Url_Mapping', async () => {
|
||||
settings.set('Outlook_Calendar_Url_Mapping', 'invalid json');
|
||||
const userInfo = await getUserInfo(user);
|
||||
expect(userInfo.settings?.calendar).toEqual({
|
||||
outlook: {
|
||||
Enabled: true,
|
||||
Exchange_Url: 'https://outlook.office365.com/',
|
||||
Outlook_Url: 'https://outlook.office365.com/owa/#calendar/view/month',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -1,4 +1,4 @@
|
||||
import { isOAuthUser, type IUser, type IUserEmail } from '@rocket.chat/core-typings';
|
||||
import { isOAuthUser, type IUser, type IUserEmail, type IUserCalendar } from '@rocket.chat/core-typings';
|
||||
|
||||
import { settings } from '../../../settings/server';
|
||||
import { getURL } from '../../../utils/server/getURL';
|
||||
@ -25,13 +25,49 @@ const getUserPreferences = async (me: IUser): Promise<Record<string, unknown>> =
|
||||
return accumulator;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the user's calendar settings based on their email domain and the configured mapping.
|
||||
* If the email is not provided or the domain is not found in the mapping,
|
||||
* it returns the default Outlook calendar settings.
|
||||
* @param email - The user's email object, which may contain the address and verification status.
|
||||
* @returns The calendar settings for the user, including Outlook calendar settings if enabled.
|
||||
*/
|
||||
const getUserCalendar = (email: false | IUserEmail | undefined): IUserCalendar => {
|
||||
const calendarSettings: IUserCalendar = {};
|
||||
|
||||
const outlook = {
|
||||
Enabled: settings.get<boolean>('Outlook_Calendar_Enabled'),
|
||||
Exchange_Url: settings.get<string>('Outlook_Calendar_Exchange_Url'),
|
||||
Outlook_Url: settings.get<string>('Outlook_Calendar_Outlook_Url'),
|
||||
};
|
||||
|
||||
const domain = email ? email.address.split('@').pop() : undefined;
|
||||
const outlookCalendarUrlMapping = settings.get<string>('Outlook_Calendar_Url_Mapping');
|
||||
|
||||
if (domain && outlookCalendarUrlMapping && outlookCalendarUrlMapping.includes(domain)) {
|
||||
try {
|
||||
const mappingObject = JSON.parse(outlookCalendarUrlMapping);
|
||||
const mappedSettings = mappingObject[domain];
|
||||
if (mappedSettings) {
|
||||
outlook.Enabled = mappedSettings.Enabled ?? outlook.Enabled;
|
||||
outlook.Exchange_Url = mappedSettings.Exchange_Url ?? outlook.Exchange_Url;
|
||||
outlook.Outlook_Url = mappedSettings.Outlook_Url ?? outlook.Outlook_Url;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Invalid Outlook Calendar URL Mapping JSON:', error);
|
||||
}
|
||||
}
|
||||
|
||||
if (outlook.Enabled) {
|
||||
calendarSettings.outlook = outlook;
|
||||
}
|
||||
|
||||
return calendarSettings;
|
||||
};
|
||||
|
||||
export async function getUserInfo(me: IUser): Promise<
|
||||
IUser & {
|
||||
email?: string;
|
||||
settings?: {
|
||||
profile: Record<string, unknown>;
|
||||
preferences: unknown;
|
||||
};
|
||||
avatarUrl: string;
|
||||
}
|
||||
> {
|
||||
@ -48,6 +84,7 @@ export async function getUserInfo(me: IUser): Promise<
|
||||
...(await getUserPreferences(me)),
|
||||
...userPreferences,
|
||||
},
|
||||
calendar: getUserCalendar(verifiedEmail),
|
||||
},
|
||||
avatarUrl: getURL(`/avatar/${me.username}`, { cdn: false, full: true }),
|
||||
isOAuthUser: isOAuthUser(me),
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import type { ICalendarNotification, IUser } from '@rocket.chat/core-typings';
|
||||
import { useEffectEvent } from '@rocket.chat/fuselage-hooks';
|
||||
import { useSetting, useStream, useUserPreference } from '@rocket.chat/ui-contexts';
|
||||
import { useStream, useUserPreference } from '@rocket.chat/ui-contexts';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { imperativeModal } from '../../lib/imperativeModal';
|
||||
@ -8,7 +8,6 @@ import OutlookCalendarEventModal from '../../views/outlookCalendar/OutlookCalend
|
||||
|
||||
export const useNotificationUserCalendar = (user: IUser) => {
|
||||
const requireInteraction = useUserPreference('desktopNotificationRequireInteraction');
|
||||
const outLookEnabled = useSetting('Outlook_Calendar_Enabled');
|
||||
const notifyUserStream = useStream('notify-user');
|
||||
|
||||
const notifyUserCalendar = useEffectEvent(async (notification: ICalendarNotification) => {
|
||||
@ -34,10 +33,10 @@ export const useNotificationUserCalendar = (user: IUser) => {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!user?._id || !outLookEnabled) {
|
||||
if (!user?._id || !user.settings?.calendar?.outlook?.Enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
return notifyUserStream(`${user._id}/calendar`, notifyUserCalendar);
|
||||
}, [notifyUserCalendar, notifyUserStream, outLookEnabled, user?._id]);
|
||||
}, [notifyUserCalendar, notifyUserStream, user.settings?.calendar?.outlook?.Enabled, user?._id]);
|
||||
};
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { useSetting } from '@rocket.chat/ui-contexts';
|
||||
import { useUser } from '@rocket.chat/ui-contexts';
|
||||
import { lazy, useMemo } from 'react';
|
||||
|
||||
import type { RoomToolboxActionConfig } from '../../views/room/contexts/RoomToolboxContext';
|
||||
@ -6,10 +6,10 @@ import type { RoomToolboxActionConfig } from '../../views/room/contexts/RoomTool
|
||||
const OutlookEventsRoute = lazy(() => import('../../views/outlookCalendar/OutlookEventsRoute'));
|
||||
|
||||
export const useOutlookCalenderRoomAction = () => {
|
||||
const enabled = useSetting('Outlook_Calendar_Enabled', false);
|
||||
const user = useUser();
|
||||
|
||||
return useMemo((): RoomToolboxActionConfig | undefined => {
|
||||
if (!enabled) {
|
||||
if (!user?.settings?.calendar?.outlook?.Enabled) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@ -21,5 +21,5 @@ export const useOutlookCalenderRoomAction = () => {
|
||||
tabComponent: OutlookEventsRoute,
|
||||
order: 999,
|
||||
};
|
||||
}, [enabled]);
|
||||
}, [user?.settings?.calendar?.outlook?.Enabled]);
|
||||
};
|
||||
|
||||
@ -2,7 +2,7 @@ import type { INotificationDesktop } from '@rocket.chat/core-typings';
|
||||
import type { SelectOption } from '@rocket.chat/fuselage';
|
||||
import { AccordionItem, Button, Field, FieldGroup, FieldHint, FieldLabel, FieldRow, Select, ToggleSwitch } from '@rocket.chat/fuselage';
|
||||
import type { TranslationKey } from '@rocket.chat/ui-contexts';
|
||||
import { useSetting, useUserPreference } from '@rocket.chat/ui-contexts';
|
||||
import { useSetting, useUserPreference, useUser } from '@rocket.chat/ui-contexts';
|
||||
import { useCallback, useEffect, useId, useMemo, useState } from 'react';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@ -22,6 +22,7 @@ const emailNotificationOptionsLabelMap = {
|
||||
|
||||
const PreferencesNotificationsSection = () => {
|
||||
const { t, i18n } = useTranslation();
|
||||
const user = useUser();
|
||||
|
||||
const [notificationsPermission, setNotificationsPermission] = useState<NotificationPermission>();
|
||||
|
||||
@ -36,7 +37,6 @@ const PreferencesNotificationsSection = () => {
|
||||
const loginEmailEnabled = useSetting('Device_Management_Enable_Login_Emails');
|
||||
const allowLoginEmailPreference = useSetting('Device_Management_Allow_Login_Email_preference');
|
||||
const showNewLoginEmailPreference = loginEmailEnabled && allowLoginEmailPreference;
|
||||
const showCalendarPreference = useSetting('Outlook_Calendar_Enabled');
|
||||
const showMobileRinging = useSetting('VideoConf_Mobile_Ringing');
|
||||
const notify = useNotification();
|
||||
|
||||
@ -95,6 +95,8 @@ const PreferencesNotificationsSection = () => {
|
||||
const enableMobileRingingId = useId();
|
||||
const desktopNotificationsLabelId = useId();
|
||||
|
||||
const showCalendarPreference = user?.settings?.calendar?.outlook?.Enabled;
|
||||
|
||||
return (
|
||||
<AccordionItem title={t('Notifications')}>
|
||||
<FieldGroup>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { Box, States, StatesIcon, StatesTitle, StatesSubtitle, ButtonGroup, Button, Throbber } from '@rocket.chat/fuselage';
|
||||
import { useResizeObserver } from '@rocket.chat/fuselage-hooks';
|
||||
import { useTranslation, useSetting } from '@rocket.chat/ui-contexts';
|
||||
import { useTranslation, useUser } from '@rocket.chat/ui-contexts';
|
||||
import type { ReactElement } from 'react';
|
||||
import { Virtuoso } from 'react-virtuoso';
|
||||
|
||||
@ -27,7 +27,7 @@ type OutlookEventsListProps = {
|
||||
|
||||
const OutlookEventsList = ({ onClose, changeRoute }: OutlookEventsListProps): ReactElement => {
|
||||
const t = useTranslation();
|
||||
const outlookUrl = useSetting('Outlook_Calendar_Outlook_Url', '');
|
||||
const user = useUser();
|
||||
const { authEnabled, isError, error } = useOutlookAuthentication();
|
||||
|
||||
const hasOutlookMethods = !(isError && error instanceof NotOnDesktopError);
|
||||
@ -43,6 +43,8 @@ const OutlookEventsList = ({ onClose, changeRoute }: OutlookEventsListProps): Re
|
||||
const calendarEvents = calendarListResult.data;
|
||||
const total = calendarEvents?.length || 0;
|
||||
|
||||
const outlookUrl = user?.settings?.calendar?.outlook?.Outlook_Url;
|
||||
|
||||
return (
|
||||
<ContextualbarDialog>
|
||||
<ContextualbarHeader>
|
||||
|
||||
@ -18,12 +18,14 @@ export function addSettings(): void {
|
||||
type: 'string',
|
||||
public: true,
|
||||
invalidValue: '',
|
||||
placeholder: 'https://exchange.example.com/',
|
||||
});
|
||||
|
||||
await this.add('Outlook_Calendar_Outlook_Url', '', {
|
||||
type: 'string',
|
||||
public: true,
|
||||
invalidValue: '',
|
||||
placeholder: 'https://exchange.example.com/owa/#path=/calendar/view/Month',
|
||||
});
|
||||
|
||||
await this.add(
|
||||
@ -41,6 +43,23 @@ export function addSettings(): void {
|
||||
public: true,
|
||||
invalidValue: false,
|
||||
});
|
||||
|
||||
/**
|
||||
* const defaultMapping = {
|
||||
* 'rocket.chat': {
|
||||
* Enabled: true,
|
||||
* Exchange_Url: 'https://owa.dev.rocket.chat/',
|
||||
* Outlook_Url: 'https://owa.dev.rocket.chat/owa/#path=/calendar'
|
||||
* },
|
||||
* };
|
||||
*/
|
||||
await this.add('Outlook_Calendar_Url_Mapping', '{}', {
|
||||
type: 'code',
|
||||
multiline: true,
|
||||
public: true,
|
||||
code: 'application/json',
|
||||
invalidValue: '{}',
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@ -41,6 +41,7 @@ export default {
|
||||
'<rootDir>/server/lib/auditServerEvents/**.spec.ts',
|
||||
'<rootDir>/server/cron/**.spec.ts',
|
||||
'<rootDir>/app/api/server/**.spec.ts',
|
||||
'<rootDir>/app/api/server/helpers/**.spec.ts',
|
||||
'<rootDir>/app/api/server/middlewares/**.spec.ts',
|
||||
],
|
||||
coveragePathIgnorePatterns: ['/node_modules/'],
|
||||
|
||||
@ -1,45 +0,0 @@
|
||||
import type { ICalendarEvent, IUser, UserStatus } from '@rocket.chat/core-typings';
|
||||
import { cronJobs } from '@rocket.chat/cron';
|
||||
|
||||
import { applyStatusChange } from './applyStatusChange';
|
||||
import { generateCronJobId } from './generateCronJobId';
|
||||
import { handleOverlappingEvents } from './handleOverlappingEvents';
|
||||
import { settings } from '../../../../app/settings/server';
|
||||
|
||||
export async function setupAppointmentStatusChange(
|
||||
eventId: ICalendarEvent['_id'],
|
||||
uid: IUser['_id'],
|
||||
startTime: Date,
|
||||
endTime?: Date,
|
||||
status?: UserStatus,
|
||||
shouldScheduleRemoval?: boolean,
|
||||
): Promise<void> {
|
||||
const hasBusyStatusSetting = settings.get<boolean>('Calendar_BusyStatus_Enabled');
|
||||
if (!endTime || !hasBusyStatusSetting) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (shouldScheduleRemoval) {
|
||||
const { shouldProceed } = await handleOverlappingEvents(eventId, uid, startTime, endTime, status);
|
||||
if (!shouldProceed) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const scheduledTime = shouldScheduleRemoval ? startTime : endTime;
|
||||
const cronJobId = generateCronJobId(eventId, uid, 'status');
|
||||
|
||||
if (await cronJobs.has(cronJobId)) {
|
||||
await cronJobs.remove(cronJobId);
|
||||
}
|
||||
|
||||
await cronJobs.addAtTimestamp(cronJobId, scheduledTime, async () => {
|
||||
await applyStatusChange({ eventId, uid, startTime, endTime, status, shouldScheduleRemoval });
|
||||
|
||||
if (!shouldScheduleRemoval) {
|
||||
if (await cronJobs.has('calendar-next-status-change')) {
|
||||
await cronJobs.remove('calendar-next-status-change');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -157,11 +157,20 @@ export interface IUserEmail {
|
||||
verified?: boolean;
|
||||
}
|
||||
|
||||
export interface IOutlook {
|
||||
Enabled: boolean;
|
||||
Exchange_Url: string;
|
||||
Outlook_Url: string;
|
||||
}
|
||||
|
||||
export interface IUserCalendar {
|
||||
outlook?: IOutlook;
|
||||
}
|
||||
|
||||
export interface IUserSettings {
|
||||
profile?: any;
|
||||
preferences?: {
|
||||
[key: string]: any;
|
||||
};
|
||||
profile?: Record<string, unknown>;
|
||||
preferences?: Record<string, any>;
|
||||
calendar?: IUserCalendar;
|
||||
}
|
||||
|
||||
export interface IGetRoomRoles {
|
||||
|
||||
@ -883,6 +883,8 @@
|
||||
"CROWD_Allow_Custom_Username": "Allow custom username in Rocket.Chat",
|
||||
"CROWD_Reject_Unauthorized": "Reject Unauthorized",
|
||||
"CSV": "CSV",
|
||||
"Calendar_BusyStatus_Enabled": "Outlook calendar status sync",
|
||||
"Calendar_BusyStatus_Enabled_Description": "Automatically sets user status to busy during scheduled Outlook meetings and returns it to the previous status afterward.",
|
||||
"Calendar_MeetingUrl_Regex": "Meeting url Regular Expression",
|
||||
"Calendar_MeetingUrl_Regex_Description": "Expression used to detect meeting URLs in event descriptions. The first matching group with a valid url will be used. HTML encoded urls will be decoded automatically.",
|
||||
"Calendar_settings": "Calendar settings",
|
||||
@ -3836,9 +3838,11 @@
|
||||
"Outlook_Calendar": "Outlook Calendar",
|
||||
"Outlook_Calendar_Enabled": "Enabled",
|
||||
"Outlook_Calendar_Exchange_Url": "Exchange URL",
|
||||
"Outlook_Calendar_Exchange_Url_Description": "Host URL for the EWS api.",
|
||||
"Outlook_Calendar_Exchange_Url_Description": "Default Host URL for the EWS api.",
|
||||
"Outlook_Calendar_Outlook_Url": "Outlook URL",
|
||||
"Outlook_Calendar_Outlook_Url_Description": "URL used to launch the Outlook web app.",
|
||||
"Outlook_Calendar_Outlook_Url_Description": "Default URL used to launch the Outlook web app.",
|
||||
"Outlook_Calendar_Url_Mapping": "Domain to Outlook URL mapping",
|
||||
"Outlook_Calendar_Url_Mapping_Description": "Map the domain of the calendar to the Outlook URL. This is useful if you have multiple domains and want to use different Outlook URLs for each.",
|
||||
"Outlook_Sync_Failed": "Failed to load outlook events.",
|
||||
"Outlook_Sync_Success": "Outlook events synchronized.",
|
||||
"Outlook_authentication": "Outlook authentication",
|
||||
|
||||
Loading…
Reference in New Issue
Block a user