From 03a6024cd3d245084fab6bbfd271fef90288a8aa Mon Sep 17 00:00:00 2001 From: Gijs de Man Date: Sun, 20 Aug 2023 17:58:01 +0200 Subject: [PATCH] Refactor Make code more understandable and readable by the user of better variable naming and typing. --- src/dovecotAPI.ts | 140 +++--- src/fileDB.ts | 204 --------- src/index.ts | 519 +++++++++++++---------- src/localUserDatabase.ts | 215 ++++++++++ src/mailcowAPI.ts | 89 ++-- src/{mailcowDB.ts => mailcowDatabase.ts} | 43 +- src/types.ts | 24 +- tsconfig.tsbuildinfo | 2 +- 8 files changed, 643 insertions(+), 593 deletions(-) delete mode 100644 src/fileDB.ts create mode 100644 src/localUserDatabase.ts rename src/{mailcowDB.ts => mailcowDatabase.ts} (55%) diff --git a/src/dovecotAPI.ts b/src/dovecotAPI.ts index d8788c5..5e06a9c 100644 --- a/src/dovecotAPI.ts +++ b/src/dovecotAPI.ts @@ -1,133 +1,133 @@ import axios, { AxiosInstance } from 'axios'; import { - ContainerConfig, - DoveadmExchangeResult, - DoveadmRequestData, - DoveadmResponseExchange, - DoveadmRights, - MailcowPermissions, + DovecotData, + DovecotRequestData, + DovecotPermissions, + ActiveDirectoryPermissions, } from './types'; +import { containerConfig } from './index'; let dovecotClient: AxiosInstance; -export async function initializeDovecotAPI(config: ContainerConfig): Promise { +/** + * Initialize the Dovecot API + */ +export async function initializeDovecotAPI(): Promise { dovecotClient = axios.create({ - // baseURL: `${config.DOVEADM_API_HOST}/doveadm/v1`, baseURL: 'http://172.22.1.250:9000/doveadm/v1', headers: { 'Content-Type': 'text/plain', - 'Authorization': `X-Dovecot-API ${Buffer.from(config.DOVEADM_API_KEY).toString('base64')}`, + 'Authorization': `X-Dovecot-API ${Buffer.from(containerConfig.DOVEADM_API_KEY).toString('base64')}`, }, }); } /** - * Get all mailboxes of an email - * @param email - email to get all inboxes from + * Get all mailbox subfolders of a mail + * @param mail - email to get all subfolders from */ -async function getMailboxes(email: string): Promise { - // Get all mailboxes - const response = (await dovecotClient.post( +async function getMailboxSubFolders(mail: string): Promise { + const mailboxData: DovecotData[] = ((await dovecotClient.post( '', [[ 'mailboxList', { - 'user': email, + 'user': mail, }, - `mailboxList_${email}`, + `mailboxList_${mail}`, ]], - )).data as DoveadmResponseExchange; + )).data)[0][1]; - // Convert response to array of mailboxes - const mailboxObjects: DoveadmExchangeResult[] = response[0][1]; + let subFolders: string[] = []; + for (let subFolder of mailboxData) { + if (subFolder.mailbox.startsWith('Shared')) continue; + subFolders.push(subFolder.mailbox); + } - return mailboxObjects.filter(function (item) { - return !item.mailbox.startsWith('Shared'); - }).map((item: DoveadmExchangeResult) => { - return item.mailbox; - }); + return subFolders; } /** - * Set read and write permissions in doveadm - * @param email - mailbox for which permissions should be set - * @param users - users that will be getting permissions to email - * @param type - permissions that will be set - * @param remove - whether permissions should be removed or added + * Set read and write permissions in dovecot + * @param mail - mail for which permissions should be set + * @param users - users that will be getting permissions to the above mail + * @param permission - permissions that will be set + * @param removePermission - whether permissions should be removed or added */ -export async function setMailPerm(email: string, users: string[], type: MailcowPermissions, remove: boolean) { - let mailboxes: string[] = []; +export async function setDovecotPermissions(mail: string, users: string[], permission: ActiveDirectoryPermissions, removePermission: boolean) { + let mailboxSubFolders: string[] = []; + let permissionTag; - let tag; - if (type == MailcowPermissions.mailPermROInbox) { - mailboxes = mailboxes.concat(['INBOX', 'Inbox']); - tag = 'PermROInbox'; + if (permission == ActiveDirectoryPermissions.mailPermROInbox) { + mailboxSubFolders = mailboxSubFolders.concat(['INBOX', 'Inbox']); + permissionTag = 'PermROInbox'; } - if (type == MailcowPermissions.mailPermROSent) { - if (tag === null) { - tag = 'PermROSent'; + if (permission == ActiveDirectoryPermissions.mailPermROSent) { + if (permissionTag === null) { + permissionTag = 'PermROSent'; } else { - tag = 'PermROInboxSent'; + permissionTag = 'PermROInboxSent'; } - mailboxes.push('Sent'); + mailboxSubFolders.push('Sent'); } - if (type == MailcowPermissions.mailPermRO || MailcowPermissions.mailPermRW) { - mailboxes = await getMailboxes(email); - tag = 'PermRO'; + if (permission == ActiveDirectoryPermissions.mailPermRO || ActiveDirectoryPermissions.mailPermRW) { + mailboxSubFolders = await getMailboxSubFolders(mail); + permissionTag = 'PermRO'; } - // Create one big request for all mailboxes and users that should be added - const requests = []; - for (const mailbox of mailboxes) { + // Dovecot API requests are very unclear and badly documented + // The idea; you can create an array of requests and send it as one big request + const dovecotRequests : DovecotRequestData[] = []; + for (const subFolder of mailboxSubFolders) { for (const user of users) { let rights = [ - DoveadmRights.lookup, - DoveadmRights.read, - DoveadmRights.write, - DoveadmRights.write_seen, + DovecotPermissions.lookup, + DovecotPermissions.read, + DovecotPermissions.write, + DovecotPermissions.write_seen, ]; - if (type === MailcowPermissions.mailPermRW) { + if (permission === ActiveDirectoryPermissions.mailPermRW) { rights = rights.concat([ - DoveadmRights.write_deleted, - DoveadmRights.insert, - DoveadmRights.post, - DoveadmRights.expunge, - DoveadmRights.create, - DoveadmRights.delete, + DovecotPermissions.write_deleted, + DovecotPermissions.insert, + DovecotPermissions.post, + DovecotPermissions.expunge, + DovecotPermissions.create, + DovecotPermissions.delete, ]); } - const request: DoveadmRequestData = [ - // Check if users should be removed or added - remove ? 'aclRemove' : 'aclSet', + const dovecotRequest: DovecotRequestData = [ + removePermission ? 'aclRemove' : 'aclSet', { - 'user': email, + 'user': mail, 'id': `user=${user}`, - 'mailbox': mailbox, + 'mailbox': subFolder, 'right': rights, }, - type === MailcowPermissions.mailPermRW ? `PermRW_${email}_${user}` : `${tag}_${email}_${user}`, + permission === ActiveDirectoryPermissions.mailPermRW ? `PermRW_${mail}_${user}` : `${permissionTag}_${mail}_${user}`, ]; - requests.push(request); + dovecotRequests.push(dovecotRequest); } } - if (requests.length > 25) { - const chunkSize = 25; - for (let i = 0; i < chunkSize; i += chunkSize) { + // There is a max size of the requests + // Break them up in smaller pieces if necessary + const dovecotMaxRequestSize: number = 25; + if (dovecotRequests.length > dovecotMaxRequestSize) { + for (let i: number = 0; i < dovecotMaxRequestSize; i += dovecotMaxRequestSize) { await dovecotClient.post( - '', requests.slice(i, i + chunkSize), + '', dovecotRequests.slice(i, i + dovecotMaxRequestSize), ); } } else { - // Post request await dovecotClient.post( - '', requests, + '', dovecotRequests, ); } } diff --git a/src/fileDB.ts b/src/fileDB.ts deleted file mode 100644 index 28e8cf7..0000000 --- a/src/fileDB.ts +++ /dev/null @@ -1,204 +0,0 @@ -import { Repository, Not, DataSource } from 'typeorm'; -import { Users } from './entities/User'; -import fs from 'fs'; -import { MailcowPermissions, ACLResults, ActiveUserSetting, UserDataDB } from './types'; - -// Connection options for the DB -const dataSource = new DataSource({ - type: 'sqlite', - database: './db/ldap-mailcow.sqlite3', - entities: [ - Users, - ], - synchronize: true, -}); - -let userRepository: Repository; -let sessionTime: number = new Date().getTime(); - -export function setSessionTime(): void { - sessionTime = new Date().getTime(); -} - -/** - * Initialize database connection. Setup database if it does not yet exist - */ -export async function initializeFileDB(): Promise { - if (!fs.existsSync('./db/ldap-mailcow.sqlite3')) - fs.writeFileSync('./db/ldap-mailcow.sqlite3', ''); - dataSource.initialize().catch((error) => console.log(error)); - userRepository = dataSource.getRepository(Users); -} - -/** - * Get all users from DB that have not been checked in current session but are active - */ -export async function getUncheckedActiveUsers(): Promise { - return Promise.resolve(userRepository.find({ - select: ['email'], - where: { - lastSeen: Not(sessionTime), - active: Not(0), - }, - })); -} - -/** - * Add a user to the DB - * @param email - mail entry in the database - * @param displayName - * @param active - whether user is active - */ -export async function addUserDB(email: string, displayName: string, active: ActiveUserSetting): Promise { - const user: Users = Object.assign(new Users(), { - email: email, - active: active, - displayName: displayName, - inactiveCount: 0, - mailPermRO: '', - changedRO: 0, - mailPermRW: '', - changedRW: 0, - mailPermROInbox: '', - changedROInbox: 0, - mailPermROSent: '', - changedROSent: 0, - mailPermSOB: '', - newMailPermSOB: '', - lastSeen: sessionTime, - }); - await userRepository.save(user); -} - -/** - * Get a user data from database - * @param email - mail from to be retrieved user - */ -export async function checkUserDB(email: string): Promise { - const dbUserData: UserDataDB = { - exists: false, - isActive: 0, - inactiveCount: 0, - }; - - // Find first user with email - const user: Users | null = await userRepository.findOne({ - where: { - email: email, - }, - }); - - // Check if user exists, if not, return immediately - if (user === null) { - return dbUserData; - } else { - // Update last time user has been checked - user.lastSeen = sessionTime; - await userRepository.update(user.email, user); - - // Return information of user - dbUserData.exists = true; - dbUserData.isActive = user.active; - dbUserData.inactiveCount = user.inactiveCount; - return dbUserData; - } -} - -/** - * Change user activity status in the DB - * @param email - email of user - * @param active - activity of user - * @param inactiveCount - number of times user has been inactive - */ -export async function activityUserDB(email: string, active: ActiveUserSetting, inactiveCount: number): Promise { - // Retrieve user with email - const user: Users = await userRepository.findOneOrFail({ - where: { - email: email, - }, - }); - // Set new activity of user - user.active = active; - user.inactiveCount = inactiveCount; - await userRepository.update(user.email, user); -} - -/** - * Update user's SOB - * @param email - email of user - * @param SOBEmail - acl to check - */ -export async function createSOBDB(email: string, SOBEmail: string): Promise { - // Retrieve user with email - const user: Users = await userRepository.findOneOrFail({ - where: { - email: email, - }, - }); - - // Check if permissions for ACL are set - const SOB = !user.newMailPermSOB ? [] : user.newMailPermSOB.split(';'); - - // Check if sob mail is in list (it should not be, but checking does not hurt) - if (SOB.indexOf(SOBEmail) === -1) { - SOB.push(SOBEmail); - user.newMailPermSOB = SOB.join(';'); - await userRepository.update(user.email, user); - } -} - -export async function getChangedSOBDB(): Promise { - // First, check all users that actually have changed - const users = await userRepository.find(); - const changedUsers : Users[] = []; - - for (const user of users) { - if (user.newMailPermSOB != user.mailPermSOB) { - console.log(`SOB of ${user.email} changed from ${user.mailPermSOB} to ${user.newMailPermSOB}`); - user.mailPermSOB = user.newMailPermSOB; - changedUsers.push(user); - } - user.newMailPermSOB = ''; - await userRepository.update(user.email, user); - } - - return changedUsers; -} - - -/** - * Update user's ACLs - * @param email - email of user - * @param newUsers - acl to check - * @param permission - type of permission to check - */ -export async function updatePermissionsDB(email: string, newUsers: string[], permission: MailcowPermissions): Promise { - // Keep track of changes in permissions - const updatedUsers: ACLResults = { - newUsers: [], - removedUsers: [], - }; - - // Find first user with email - const user: Users = await userRepository.findOneOrFail({ - where: { - email: email, - }, - }); - - // Get existing permissions from mailbox - if (!newUsers) newUsers = []; - if (!Array.isArray(newUsers)) newUsers = [newUsers]; - - const removedUsers = !user ? [] : user[permission].split(';'); - - // Filter for new users, also filter empty entries - updatedUsers.newUsers = newUsers.filter(x => !removedUsers.includes(x) && x != ''); - updatedUsers.removedUsers = removedUsers.filter(x => !newUsers.includes(x) && x != ''); - - // Put new user list in database - user[permission] = newUsers.join(';'); - await userRepository.update(user.email, user); - - return updatedUsers; -} diff --git a/src/index.ts b/src/index.ts index 7391e86..f94ad86 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,34 +1,45 @@ import { Client } from 'ldapts'; import { - activityUserDB, - addUserDB, - checkUserDB, createSOBDB, - getChangedSOBDB, - getUncheckedActiveUsers, - initializeFileDB, - setSessionTime, - updatePermissionsDB, -} from './fileDB'; -import { replaceInFile, ReplaceInFileConfig } from 'replace-in-file'; + updateLocalUserActivity, + createLocalUser, + getLocalUser, + editLocalUserPermissions, + getUpdateSOBLocalUsers, + getUncheckedLocalActiveUsers, + initializeLocalUserDatabase, + updateLocalUserPermissions, editLocalUserDisplayName, +} from './localUserDatabase'; +import { + replaceInFile, + ReplaceInFileConfig, +} from 'replace-in-file'; import fs, { PathLike } from 'fs'; import path from 'path'; -import { SearchResult } from 'ldapts/Client'; -import { addUserAPI, checkUserAPI, editUserAPI, initializeMailcowAPI } from './mailcowAPI'; - import { - ACLResults, + createMailcowUser, + getMailcowUser, + editMailcowUser, + initializeMailcowAPI, +} from './mailcowAPI'; +import { + ChangedUsers, ActiveUserSetting, ContainerConfig, - LDAPResults, - MailcowPermissions, - UserDataAPI, - UserDataDB, + ActiveDirectoryUser, + ActiveDirectoryPermissions, + MailcowUserData, + LocalUserData, } from './types'; -import { initializeDovecotAPI, setMailPerm } from './dovecotAPI'; -import { editUserSignature, initializeMailcowDB } from './mailcowDB'; +import { + initializeDovecotAPI, + setDovecotPermissions, +} from './dovecotAPI'; +import { + editUserSignatures, + initializeMailcowDatabase, +} from './mailcowDatabase'; -// Set all default variables -const config: ContainerConfig = { +export const containerConfig: ContainerConfig = { LDAP_URI: '', LDAP_BIND_DN: '', LDAP_BIND_DN_PASSWORD: '', @@ -45,95 +56,112 @@ const config: ContainerConfig = { DOVEADM_API_KEY: '', DOVEADM_API_HOST: '', }; +export const sessionTime: number = new Date().getTime(); +const consoleLogLine: string = '-'.repeat(40); -let LDAPConnector: Client; +let activeDirectoryConnector: Client; +let activeDirectoryUsers: ActiveDirectoryUser[] = []; -export async function getLDAPDisplayName(email: string) : Promise { - const LDAPUsers = (await LDAPConnector.search(config.LDAP_BASE_DN, { + +/** + * Search active directory users on mail and return display name + * @param mail - mail to search for in Active Directory + */ +export async function getActiveDirectoryDisplayName(mail: string) : Promise { + const activeDirectoryUser: ActiveDirectoryUser[] = (await activeDirectoryConnector.search(containerConfig.LDAP_BASE_DN, { scope: 'sub', - filter: `(&(objectClass=user)(objectCategory=person)(mail=${email})`, + filter: `(&(objectClass=user)(objectCategory=person)(mail=${mail})`, attributes: ['displayName'], - })).searchEntries as unknown as LDAPResults[]; + })).searchEntries as unknown as ActiveDirectoryUser[]; - return LDAPUsers[0].displayName; + // There should only be one resulting entry + return activeDirectoryUser[0].displayName; } -async function getUserMails(users: string[], skipEntry: LDAPResults): Promise { - const result = []; + +/** + * Search active directory users on DN and return their mails + * @param users - list of DN of users of which to return their mail + * @param skipUser - user to not return in the array of mails + */ +async function getActiveDirectoryMails(users: string[], skipUser: ActiveDirectoryUser): Promise { + const activeDirectoryMails: string[] = []; for (const user of users) { - const userResults: SearchResult = await LDAPConnector.search(user, { + const activeDirectoryUser: ActiveDirectoryUser[] = (await activeDirectoryConnector.search(user, { scope: 'sub', attributes: ['mail'], - }); - const userMail = userResults.searchEntries as unknown as LDAPResults[]; - if (userMail[0].mail != skipEntry.mail) result.push(userMail[0].mail); + })) as unknown as ActiveDirectoryUser[]; + + // We do not want to set permissions for owner of the mailbox + // There should only be one resulting entry + if (activeDirectoryUser[0].mail != skipUser.mail) activeDirectoryMails.push(activeDirectoryUser[0].mail); } - return result; + return activeDirectoryMails; } /** - * Sync all the permissions for ACLs - * @param entry - current mailbox (users will get permission to this mailbox) - * @param type - type of permission being considered + * Synchronize all the ACL of a user with Active Directory + * @param activeDirectoryUser - user to sync with Active Directory + * @param permission - specific permissions to sync on */ -async function syncUserPermissions(entry: LDAPResults, type: MailcowPermissions): Promise { - // Get mail permissions group - const permissionResults: SearchResult = await LDAPConnector.search(entry[type], { +async function synchronizeUserACL(activeDirectoryUser: ActiveDirectoryUser, permission: ActiveDirectoryPermissions): Promise { + const activeDirectoryPermissionGroup = (await activeDirectoryConnector.search(activeDirectoryUser[permission], { scope: 'sub', attributes: ['memberFlattened'], - }); + })).searchEntries[0] as unknown as ActiveDirectoryUser; - // Update all the permissions - await updatePermissionsDB(entry.mail, - (permissionResults.searchEntries[0] as unknown as LDAPResults).memberFlattened, type) - .then(async (results: ACLResults) => { - // Map newUsers to actual emails - if (results.newUsers.length != 0) { - results.newUsers = await getUserMails(results.newUsers, entry); + await updateLocalUserPermissions(activeDirectoryUser.mail, activeDirectoryPermissionGroup.memberFlattened, permission) + .then(async (changedUsers: ChangedUsers) => { + if (changedUsers.newUsers.length != 0) { + changedUsers.newUsers = await getActiveDirectoryMails(changedUsers.newUsers, activeDirectoryUser); - console.log(`User(s) ${results.newUsers} added to ${entry.mail} for ${type}`); - await setMailPerm(entry.mail, results.newUsers, type, false); + console.log(`User(s) ${changedUsers.newUsers} added to ${activeDirectoryUser.mail} for ${permission}`); + await setDovecotPermissions(activeDirectoryUser.mail, changedUsers.newUsers, permission, false); } - // Map oldUsers to actual emails - if (results.removedUsers.length != 0) { - results.removedUsers = await getUserMails(results.removedUsers, entry); + if (changedUsers.removedUsers.length != 0) { + changedUsers.removedUsers = await getActiveDirectoryMails(changedUsers.removedUsers, activeDirectoryUser); - console.log(`User(s) ${results.removedUsers} removed from ${entry.mail} for ${type}`); - await setMailPerm(entry.mail, results.removedUsers, type, true); + console.log(`User(s) ${changedUsers.removedUsers} removed from ${activeDirectoryUser.mail} for ${permission}`); + await setDovecotPermissions(activeDirectoryUser.mail, changedUsers.removedUsers, permission, true); } }, ); } -async function syncUserSOB(entry: LDAPResults): Promise { - const SOBResults: SearchResult = await LDAPConnector.search(entry[MailcowPermissions.mailPermSOB], { + +/** + * Synchronize all the SOB of a user with Active Directory + * @param activeDirectoryGroup - group to sync with Active Directory + */ +async function synchronizeUserSOB(activeDirectoryGroup: ActiveDirectoryUser): Promise { + // Should always be one entry + const activeDirectoryPermissionGroup: ActiveDirectoryUser = ((await activeDirectoryConnector.search(activeDirectoryGroup[ActiveDirectoryPermissions.mailPermSOB], { scope: 'sub', attributes: ['memberFlattened'], - }); + })).searchEntries)[0] as unknown as ActiveDirectoryUser; - // Construct list in database with DN of all committees they are in - // Get existing list of committees, add new DN as string - for (const members of SOBResults.searchEntries as unknown as LDAPResults[]) { - // For some reason a single entry is returned as string, so turn it into an array - if (!Array.isArray(members.memberFlattened)) - members.memberFlattened = [members.memberFlattened]; - for (const member of members.memberFlattened) { - const memberResults = (await LDAPConnector.search(member, { - scope: 'sub', - attributes: ['mail'], - })).searchEntries as unknown as LDAPResults[]; - await createSOBDB(memberResults[0].mail, entry.mail); - } + // Singular entries are possible, so turn them into an array + if (!Array.isArray(activeDirectoryPermissionGroup.memberFlattened)) + activeDirectoryPermissionGroup.memberFlattened = [activeDirectoryPermissionGroup.memberFlattened]; + + // All users are given as DN, so we have to get their mail first + for (const activeDirectoryUserDN of activeDirectoryPermissionGroup.memberFlattened) { + const activeDirectoryUserMail: ActiveDirectoryUser = ((await activeDirectoryConnector.search(activeDirectoryUserDN, { + scope: 'sub', + attributes: ['mail'], + })).searchEntries)[0] as unknown as ActiveDirectoryUser; + // Own group has to be skipped to prevent clashing permissions + await editLocalUserPermissions(activeDirectoryUserMail.mail, activeDirectoryGroup.mail); } } + /** - * Impose the configuration of LDAP from the environment + * Create config file from environment variables. */ -function readConfig(): void { - // All required config keys +function createConfigFromEnvironment(): void { const requiredConfigKeys: string[] = [ 'LDAP-MAILCOW_LDAP_URI', 'LDAP-MAILCOW_LDAP_GC_URI', @@ -149,42 +177,34 @@ function readConfig(): void { 'DOVEADM_API_KEY', ]; - // Check if all keys are set in the environment for (const configKey of requiredConfigKeys) { - if (!(configKey in process.env)) throw new Error(`Required environment value ${configKey} is not set`); - console.log(`Required environment value ${configKey} has been set`); - + if (!(configKey in process.env)) throw new Error(`Required environment value ${configKey} is not set. `); // Add keys to local config variable - config[configKey.replace('LDAP-MAILCOW_', '') as keyof ContainerConfig] = process.env[configKey]!; + containerConfig[configKey.replace('LDAP-MAILCOW_', '') as keyof ContainerConfig] = process.env[configKey]!; } - // Check if Sogo filter is set if ('LDAP-MAILCOW_LDAP_FILTER' in process.env && !('LDAP-MAILCOW_SOGO_LDAP_FILTER' in process.env)) throw new Error('LDAP-MAILCOW_SOGO_LDAP_FILTER is required when you specify LDAP-MAILCOW_LDAP_FILTER'); - // Check if Mailcow filter is set if ('LDAP-MAILCOW_SOGO_LDAP_FILTER' in process.env && !('LDAP-MAILCOW_LDAP_FILTER' in process.env)) throw new Error('LDAP-MAILCOW_LDAP_FILTER is required when you specify LDAP-MAILCOW_SOGO_LDAP_FILTER'); - // Set Mailcow LDAP filter (has fallback value) if ('LDAP-MAILCOW_LDAP_FILTER' in process.env) - config.LDAP_FILTER = process.env['LDAP-MAILCOW_LDAP_FILTER']!; - + containerConfig.LDAP_FILTER = process.env['LDAP-MAILCOW_LDAP_FILTER']!; - // Set Sogo LDAP filter (has fallback value) if ('LDAP-MAILCOW_SOGO_LDAP_FILTER' in process.env) - config.SOGO_LDAP_FILTER = process.env['LDAP-MAILCOW_SOGO_LDAP_FILTER']!; + containerConfig.SOGO_LDAP_FILTER = process.env['LDAP-MAILCOW_SOGO_LDAP_FILTER']!; - console.log('Read and configured all environment variables'); + console.log('Successfully created config file. \n\n'); } + /** * Compare, backup and save (new) config files * @param configPath - path to original config file * @param configData - data of new config file */ function applyConfig(configPath: PathLike, configData: string): boolean { - // Check if path to config file exists if (fs.existsSync(configPath)) { // Read and compare original data from config with new data const oldConfig: string = fs.readFileSync(configPath, 'utf8'); @@ -221,6 +241,7 @@ function applyConfig(configPath: PathLike, configData: string): boolean { return true; } + /** * Replace all variables in template file with new configuration */ @@ -229,18 +250,18 @@ async function readPassDBConfig(): Promise { files: './templates/dovecot/ldap/passdb.conf', from: ['$ldap_gc_uri', '$ldap_domain', '$ldap_base_dn', '$ldap_bind_dn', '$ldap_bind_dn_password'], to: [ - config.LDAP_GC_URI, - config.LDAP_DOMAIN, - config.LDAP_BASE_DN, - config.LDAP_BIND_DN, - config.LDAP_BIND_DN_PASSWORD, + containerConfig.LDAP_GC_URI, + containerConfig.LDAP_DOMAIN, + containerConfig.LDAP_BASE_DN, + containerConfig.LDAP_BIND_DN, + containerConfig.LDAP_BIND_DN_PASSWORD, ], }; - console.log('Adjust passdb_conf template file'); await replaceInFile(options); return fs.readFileSync('./templates/dovecot/ldap/passdb.conf', 'utf8'); } + /** * Replace all variables in template file with new configuration */ @@ -249,13 +270,14 @@ async function readDovecotExtraConfig(): Promise { files: './templates/dovecot/extra.conf', from: ['$doveadm_api_key'], to: [ - config.DOVEADM_API_KEY, + containerConfig.DOVEADM_API_KEY, ], }; await replaceInFile(options); return fs.readFileSync('./templates/dovecot/extra.conf', 'utf8'); } + /** * Replace all variables in template file with new configuration */ @@ -264,11 +286,11 @@ async function readPListLDAP(): Promise { files: './templates/sogo/plist_ldap', from: ['$ldap_uri', '$ldap_base_dn', '$ldap_bind_dn', '$ldap_bind_dn_password', '$sogo_ldap_filter'], to: [ - config.LDAP_URI, - config.LDAP_BASE_DN, - config.LDAP_BIND_DN, - config.LDAP_BIND_DN_PASSWORD, - config.SOGO_LDAP_FILTER, + containerConfig.LDAP_URI, + containerConfig.LDAP_BASE_DN, + containerConfig.LDAP_BIND_DN, + containerConfig.LDAP_BIND_DN_PASSWORD, + containerConfig.SOGO_LDAP_FILTER, ], }; console.log('Adjust plist_ldap template file'); @@ -276,185 +298,218 @@ async function readPListLDAP(): Promise { return fs.readFileSync('./templates/sogo/plist_ldap', 'utf8'); } + /** - * Synchronise LDAP users with Mailcow mailboxes and users stores in local DB + * Get all users from Active Directory */ -async function syncUsers(): Promise { - - // Search for al users, use filter and only display few attributes - let LDAPUsers : LDAPResults[] = []; - let retryCount = 0; +async function getUserDataFromActiveDirectory(): Promise { + let retryCount: number = 0; + const maxRetryCount: number = parseInt(containerConfig.MAX_LDAP_RETRY_COUNT); - while (LDAPUsers.length === 0 && retryCount < parseInt(config.MAX_LDAP_RETRY_COUNT)) { - console.log(`Attempt ${retryCount} to get LDAPResults`); + // Sometimes LDAP response is empty, retry in those cases + while (activeDirectoryUsers.length === 0 && retryCount < maxRetryCount) { + if (retryCount > 0) console.warn(`Retry number ${retryCount} to get LDAPResults`); retryCount++; - LDAPUsers = (await LDAPConnector.search(config.LDAP_BASE_DN, { + + activeDirectoryUsers = (await activeDirectoryConnector.search(containerConfig.LDAP_BASE_DN, { scope: 'sub', - filter: config.LDAP_FILTER, + filter: containerConfig.LDAP_FILTER, attributes: ['mail', 'displayName', 'userAccountControl', 'mailPermRO', 'mailPermRW', 'mailPermROInbox', 'mailPermROSent', 'mailPermSOB'], - })).searchEntries as unknown as LDAPResults[]; + })).searchEntries as unknown as ActiveDirectoryUser[]; } - // Update session time - setSessionTime(); + if (retryCount === maxRetryCount) throw new Error('Ran into an issue when getting users from Active Directory.'); + console.log('Successfully got all users from Active Directory. \n\n'); +} + - // Loop over all LDAP entries - for (const entry of LDAPUsers) { +/** + * Synchronise LDAP users with Mailcow mailboxes and users stores in local DB + */ +async function synchronizeUsersWithActiveDirectory(): Promise { + for (const activeDirectoryUser of activeDirectoryUsers) { try { - // Check if LDAP user has email, if not, skip - if (!entry.mail || entry.mail.length === 0) { - continue; + if (!activeDirectoryUser.mail || activeDirectoryUser.mail.length === 0) continue; + const mail: string = activeDirectoryUser.mail; + const displayName: string = activeDirectoryUser.displayName; + // Active: 0 = no incoming mail/no login, 1 = allow both, 2 = custom state: allow incoming mail/no login + const isActive: ActiveUserSetting = (activeDirectoryUser.userAccountControl & 0b10) == 2 ? 2 : 1; + + const localUser: LocalUserData = await getLocalUser(mail); + const mailcowUser: MailcowUserData = await getMailcowUser(mail); + + if (!localUser.exists) { + console.log(`Adding local user ${mail} (active: ${isActive})`); + await createLocalUser(mail, displayName, isActive); + localUser.exists = true; + localUser.isActive = isActive; } - // Read data from LDAP - const email: string = entry.mail; - const displayName: string = entry.displayName; - // Active: 0 = no incoming mail/no login, 1 = allow both, 2 = custom state: allow incoming mail/no login - const isActive: ActiveUserSetting = (entry.userAccountControl & 0b10) == 2 ? 2 : 1; - // Read data of LDAP user van local DB and mailcow - const userDataDB: UserDataDB = await checkUserDB(email); - const userDataAPI: UserDataAPI = await checkUserAPI(email); - - // Check if user exists in DB, if not, add user to DB - if (!userDataDB.exists) { - console.log(`Added filedb user: ${email} (Active: ${isActive})`); - await addUserDB(email, displayName, isActive); - userDataDB.exists = true; - userDataDB.isActive = isActive; + if (!mailcowUser.exists) { + console.log(`Adding Mailcow user ${mail} (active: ${isActive})`); + await createMailcowUser(mail, displayName, isActive, 256); + mailcowUser.exists = true; + mailcowUser.isActive = isActive; + mailcowUser.displayName = displayName; } - // Check if user exists in Mailcow, if not, add user to Mailcow - if (!userDataAPI.exists) { - console.log(`Added Mailcow user: ${email} (Active: ${isActive})`); - await addUserAPI(email, displayName, isActive, 256); - userDataAPI.exists = true; - userDataAPI.isActive = isActive; - userDataAPI.displayName = displayName; + if (localUser.isActive !== isActive) { + console.log(`Set ${mail} to active state ${isActive} in local user database`); + await updateLocalUserActivity(mail, isActive, 0); } - // Check if user is active in DB, if not, adjust accordingly - if (userDataDB.isActive !== isActive) { - console.log(`Set ${email} to active ${isActive} in filedb`); - await activityUserDB(email, isActive, 0); + if (mailcowUser.isActive !== isActive) { + console.log(`Set ${mail} to active state ${isActive} in Mailcow`); + await editMailcowUser(mail, { active: isActive }); } - // Check if user is active in Mailcow, if not, adjust accordingly - if (userDataAPI.isActive !== isActive) { - console.log(`Set ${email} to active ${isActive} in Mailcow`); - await editUserAPI(email, { active: isActive }); + if (mailcowUser.displayName !== displayName) { + console.log(`Changed displayname for ${mail} to ${displayName} in Mailcow`); + await editMailcowUser(mail, { name: displayName }); } - // Check if user's name in Mailcow matches LDAP name, adjust accordingly - if (userDataAPI.displayName !== displayName) { - console.log(`Changed name of ${email} to ${displayName} in Mailcow`); - await editUserAPI(email, { name: displayName }); + if (localUser.displayName !== displayName) { + console.log(`Changed displayname for ${mail} to ${displayName} in local database`); + await editLocalUserDisplayName(mail, displayName); } + } catch (error) { - console.log(`Exception throw during handling of ${entry}: ${error}`); + console.error(`Ran into an issue when syncing user ${activeDirectoryUser.mail}. \n\n ${error}`); } } - // let count = 0 - - // Set all permissions for mailboxes - console.log('Setting ACL permissions'); - for (const entry of LDAPUsers) { + // Users that were not checked might have to be removed from mailcow + for (const user of await getUncheckedLocalActiveUsers()) { try { - if (entry[MailcowPermissions.mailPermROInbox].length != 0) - await syncUserPermissions(entry, MailcowPermissions.mailPermROInbox); - if (entry[MailcowPermissions.mailPermROSent].length != 0) - await syncUserPermissions(entry, MailcowPermissions.mailPermROSent); - if (entry[MailcowPermissions.mailPermRO].length != 0) - await syncUserPermissions(entry, MailcowPermissions.mailPermRO); - if (entry[MailcowPermissions.mailPermRW].length != 0) - await syncUserPermissions(entry, MailcowPermissions.mailPermRW); - if (entry[MailcowPermissions.mailPermSOB].length != 0) - await syncUserSOB(entry); + const mailcowUserData: MailcowUserData = await getMailcowUser(user.email); + const localUserData: LocalUserData = await getLocalUser(user.email); + + // We check if user has b + const inactiveCount: number = localUserData.inactiveCount; + const maxInactiveCount: number = parseInt(containerConfig.MAX_INACTIVE_COUNT); + + if (inactiveCount > maxInactiveCount) { + console.log(`Deactivated user ${user.email} in local user database, not found in LDAP`); + await updateLocalUserActivity(user.email, 0, 255); + } else { + console.log(`Increased inactive count to ${inactiveCount + 1} for ${user.email}`); + await updateLocalUserActivity(user.email, 2, inactiveCount + 1); + } + + if (mailcowUserData.isActive && localUserData.isActive === 0) { + console.log(`Deactivated user ${user.email} in Mailcow, not found in Active Directory`); + await editMailcowUser(user.email, { active: 0 }); + } } catch (error) { - console.log(`Exception throw during handling of ${entry}: ${error}`); + console.log(`Ran into an issue when checking inactivity of ${user.email}. \n\n ${error}`); } } + console.log('Successfully synced all users with Active Directory. \n\n'); +} + - // Make final changes for SOB - console.log('Changing SOB in mailcow'); - for (const entry of await getChangedSOBDB()) { +/** + * Synchronize all the permissions with Active Directory + */ +async function synchronizePermissionsWithActiveDirectory(): Promise { + for (const activeDirectoryUser of activeDirectoryUsers) { try { - console.log(`Changing SOB of ${entry.email}`); - const SOBs = entry.mailPermSOB.split(';'); - await editUserAPI(entry.email, { sender_acl: SOBs }); - await editUserSignature(entry, SOBs); + // Check if current user has corresponding permissions + // Sometimes, the mail considered is a personal account, but it can also be a shared mailbox + // (in principle this does not matter though, personal mails _could_ in principle also be shared if wanted) + if (activeDirectoryUser[ActiveDirectoryPermissions.mailPermROInbox].length != 0) + await synchronizeUserACL(activeDirectoryUser, ActiveDirectoryPermissions.mailPermROInbox); + if (activeDirectoryUser[ActiveDirectoryPermissions.mailPermROSent].length != 0) + await synchronizeUserACL(activeDirectoryUser, ActiveDirectoryPermissions.mailPermROSent); + if (activeDirectoryUser[ActiveDirectoryPermissions.mailPermRO].length != 0) + await synchronizeUserACL(activeDirectoryUser, ActiveDirectoryPermissions.mailPermRO); + if (activeDirectoryUser[ActiveDirectoryPermissions.mailPermRW].length != 0) + await synchronizeUserACL(activeDirectoryUser, ActiveDirectoryPermissions.mailPermRW); + if (activeDirectoryUser[ActiveDirectoryPermissions.mailPermSOB].length != 0) + await synchronizeUserSOB(activeDirectoryUser); } catch (error) { - console.log(`Exception throw during handling of ${entry}: ${error}`); + console.log(`Ran into an issue when syncing permissions of ${activeDirectoryUser.mail}. \n\n ${error}`); } } - // Check all users in DB that have not yet been checked and are active - console.log('Checking users that are no longer in AD'); - for (const user of await getUncheckedActiveUsers()) { + for (const activeDirectoryUser of await getUpdateSOBLocalUsers()) { try { - // Get user data from Mailcow - const userDataAPI: UserDataAPI = await checkUserAPI(user.email); - const userDataDB: UserDataDB = await checkUserDB(user.email); - const inactiveCount = userDataDB.inactiveCount; - - if (inactiveCount > parseInt(config.MAX_INACTIVE_COUNT)) { - console.log(`Deactivated user ${user.email} in filedb, not found in LDAP`); - await activityUserDB(user.email, 0, 255); - } else { - console.log(`Increased inactive count to ${inactiveCount + 1} for ${user.email}`); - await activityUserDB(user.email, 2, inactiveCount + 1); - } - - // Check if user is still active, if so, deactivate user - if (userDataAPI.isActive && userDataDB.isActive === 0) { - console.log(`Deactivated user ${user.email} in Mailcow, not found in LDAP`); - await editUserAPI(user.email, { active: 0 }); - } + console.log(`Changing SOB of ${activeDirectoryUser.email}`); + const SOBs: string[] = activeDirectoryUser.mailPermSOB.split(';'); + await editMailcowUser(activeDirectoryUser.email, { sender_acl: SOBs }); + await editUserSignatures(activeDirectoryUser, SOBs); } catch (error) { - console.log(`Exception throw during handling of ${user}: ${error}`); + console.log(`Ran into an issue when syncing send on behalf of ${activeDirectoryUser.email}. \n\n ${error}`); } } + + console.log('Successfully synced all permissions with Active Directory. \n\n'); } +/** + * Read all files, initialize all (database) connections + */ async function initializeSync(): Promise { - // Read LDAP configuration - readConfig(); + console.log(consoleLogLine + '\n READING ENVIRONMENT VARIABLES \n' + consoleLogLine); + createConfigFromEnvironment(); - // Connect to LDAP server using config - console.log('Binding LDAP'); - LDAPConnector = new Client({ - url: config.LDAP_URI, + + console.log(consoleLogLine + '\n SETTING UP CONNECTION WITH ACTIVE DIRECTORY\n' + consoleLogLine); + activeDirectoryConnector = new Client({ + url: containerConfig.LDAP_URI, }); - await LDAPConnector.bind(config.LDAP_BIND_DN, config.LDAP_BIND_DN_PASSWORD); - - // Adjust template files - console.log('Reading DB config'); - const passDBConfig: string = await readPassDBConfig(); - console.log('Reading PList'); - const pListLDAP: string = await readPListLDAP(); - console.log('Reading Dovecot config'); - // Read data in extra config file - console.log('Reading Extra config'); - const extraConfig: string = await readDovecotExtraConfig(); - - // Apply all config files, see if any changed - console.log('Applying configs'); + console.log('Successfully connected with active directory. \n\n'); + + await activeDirectoryConnector + .bind(containerConfig.LDAP_BIND_DN, containerConfig.LDAP_BIND_DN_PASSWORD) + .catch((error) => { + throw new Error('Ran into an issue when connecting to Active Directory. \n\n' + error); + }); + + + console.log(consoleLogLine + '\n ADJUSTING TEMPLATE FILES \n' + consoleLogLine); + const passDBConfig: string = await readPassDBConfig() + .catch((error) => { + throw new Error('Ran into an issue when reading passdb.conf. \n\n' + error); + }); + + const pListLDAP: string = await readPListLDAP() + .catch((error) => { + throw new Error('Ran into an issue when reading plist_ldap. \n\n' + error); + }); + + const extraConfig: string = await readDovecotExtraConfig() + .catch((error) => { + throw new Error('Ran into an issue when reading extra.conf. \n\n' + error); + }); + console.log('Successfully adjusted all template files. \n\n'); + + console.log(consoleLogLine + '\n APPLYING CONFIG FILES \n' + consoleLogLine); const passDBConfigChanged: boolean = applyConfig('./conf/dovecot/ldap/passdb.conf', passDBConfig); const extraConfigChanged: boolean = applyConfig('./conf/dovecot/extra.conf', extraConfig); const pListLDAPChanged: boolean = applyConfig('./conf/sogo/plist_ldap', pListLDAP); if (passDBConfigChanged || extraConfigChanged || pListLDAPChanged) - console.log('One or more config files have been changed, please restart dovecot-mailcow and sogo-mailcow!'); - - // Start 'connection' with database - console.log('Initializing'); - await initializeFileDB(); - await initializeMailcowDB(config); - await initializeMailcowAPI(config); - await initializeDovecotAPI(config); - - // Start sync loop every interval milliseconds - console.log('Syncing users'); - await syncUsers(); + console.warn('One or more config files have been changed, please restart dovecot-mailcow and sogo-mailcow.'); + console.log('Successfully applied all config files \n\n'); + + console.log(consoleLogLine + '\n INITIALIZING DATABASES AND API CLIENTS \n' + consoleLogLine); + await initializeLocalUserDatabase(); + await initializeMailcowDatabase(); + await initializeMailcowAPI(); + await initializeDovecotAPI(); + console.log('Successfully initialized all databases and API clients \n\n'); + + console.log(consoleLogLine + '\nGETTING USERS FROM ACTIVE DIRECTORY\n' + consoleLogLine); + await getUserDataFromActiveDirectory(); + + console.log(consoleLogLine + '\n SYNCING ALL USERS \n' + consoleLogLine); + await synchronizeUsersWithActiveDirectory(); + + console.log(consoleLogLine + '\n SYNCING ALL PERMISSIONS \n' + consoleLogLine); + await synchronizePermissionsWithActiveDirectory(); } +/** + * Start sync + */ initializeSync().then(() => console.log('Finished!')); \ No newline at end of file diff --git a/src/localUserDatabase.ts b/src/localUserDatabase.ts new file mode 100644 index 0000000..66a294e --- /dev/null +++ b/src/localUserDatabase.ts @@ -0,0 +1,215 @@ +import { Repository, Not, DataSource } from 'typeorm'; +import { Users } from './entities/User'; +import fs from 'fs'; +import { ActiveDirectoryPermissions, ChangedUsers, ActiveUserSetting, LocalUserData } from './types'; +import { sessionTime } from './index'; + +let localUserRepository: Repository; +let dataSource: DataSource; + +/** + * Initialize database connection. Setup database if it does not yet exist + */ +export async function initializeLocalUserDatabase(): Promise { + if (!fs.existsSync('./db/ldap-mailcow.sqlite3')) + fs.writeFileSync('./db/ldap-mailcow.sqlite3', ''); + + dataSource = new DataSource({ + type: 'sqlite', + database: './db/ldap-mailcow.sqlite3', + entities: [ + Users, + ], + synchronize: true, + }); + + dataSource.initialize().catch((error) => console.log(error)); + localUserRepository = dataSource.getRepository(Users); +} + + +/** + * Get all users from DB that have not been checked in current session but are active + */ +export async function getUncheckedLocalActiveUsers(): Promise { + return Promise.resolve(localUserRepository.find({ + select: ['email'], + where: { + lastSeen: Not(sessionTime), + active: Not(0), + }, + })); +} + + +/** + * Add a user to the local database + * @param mail - mail entry in the database + * @param displayName - display name of the user + * @param active - whether user is active + */ +export async function createLocalUser(mail: string, displayName: string, active: ActiveUserSetting): Promise { + const user: Users = Object.assign(new Users(), { + email: mail, + active: active, + displayName: displayName, + inactiveCount: 0, + mailPermRO: '', + changedRO: 0, + mailPermRW: '', + changedRW: 0, + mailPermROInbox: '', + changedROInbox: 0, + mailPermROSent: '', + changedROSent: 0, + mailPermSOB: '', + newMailPermSOB: '', + lastSeen: sessionTime, + }); + await localUserRepository.save(user); +} + + +/** + * Get a user data from database + * @param mail - mail from to be retrieved user + */ +export async function getLocalUser(mail: string): Promise { + const localUserData: LocalUserData = { + exists: false, + displayName: '', + isActive: 0, + inactiveCount: 0, + }; + + const localUser: Users | null = await localUserRepository.findOne({ + where: { + email: mail, + }, + }); + + if (localUser === null) { + return localUserData; + } else { + localUser.lastSeen = sessionTime; + await localUserRepository.update(localUser.email, localUser); + + localUserData.exists = true; + localUserData.displayName = localUser.displayName; + localUserData.isActive = localUser.active; + localUserData.inactiveCount = localUser.inactiveCount; + return localUserData; + } +} + + +/** + * Change user activity status in the local database + * @param mail - email of user + * @param active - activity of user + * @param inactiveCount - number of times user has been inactive + */ +export async function updateLocalUserActivity(mail: string, active: ActiveUserSetting, inactiveCount: number): Promise { + const user: Users = await localUserRepository.findOneOrFail({ + where: { + email: mail, + }, + }); + user.active = active; + user.inactiveCount = inactiveCount; + await localUserRepository.update(user.email, user); +} + + +/** + * Change user display name in the local database + * @param mail - email of user + * @param displayName - display name to be set + */ +export async function editLocalUserDisplayName(mail: string, displayName: string): Promise { + const user: Users = await localUserRepository.findOneOrFail({ + where: { + email: mail, + }, + }); + user.displayName = displayName; + await localUserRepository.update(user.email, user); +} + + +/** + * Update user's SOB in the local database + * @param mail - email of user + * @param SOBEmail - email to check SOB for + */ +export async function editLocalUserPermissions(mail: string, SOBEmail: string): Promise { + const user: Users = await localUserRepository.findOneOrFail({ + where: { + email: mail, + }, + }); + + // Check if permissions for ACL are set + const SOB: string[] = !user.newMailPermSOB ? [] : user.newMailPermSOB.split(';'); + + // Check if sob mail is in list (it should not be, but checking does not hurt) + if (SOB.indexOf(SOBEmail) === -1) { + SOB.push(SOBEmail); + user.newMailPermSOB = SOB.join(';'); + await localUserRepository.update(user.email, user); + } +} + + +/** + * Get all local users of which the SOB has changed in this session + */ +export async function getUpdateSOBLocalUsers(): Promise { + const users: Users[] = await localUserRepository.find(); + const changedUsers : Users[] = []; + + for (const user of users) { + if (user.newMailPermSOB != user.mailPermSOB) { + console.log(`SOB of ${user.email} changed from ${user.mailPermSOB} to ${user.newMailPermSOB}.`); + user.mailPermSOB = user.newMailPermSOB; + changedUsers.push(user); + } + user.newMailPermSOB = ''; + await localUserRepository.update(user.email, user); + } + + return changedUsers; +} + + +/** + * Update local user permissions + * @param mail - email of user + * @param newUsers - acl to check + * @param permission - type of permission to change + */ +export async function updateLocalUserPermissions(mail: string, newUsers: string[], permission: ActiveDirectoryPermissions): Promise { + const changedUsers: ChangedUsers = { + newUsers: [], + removedUsers: [], + }; + + const user: Users = await localUserRepository.findOneOrFail({ + where: { + email: mail, + }, + }); + + // Sometimes, new users can be null or a singular item + if (!newUsers) newUsers = []; + if (!Array.isArray(newUsers)) newUsers = [newUsers]; + + // Filter for users, also filter empty entries + const removedUsers : string[] = !user ? [] : user[permission].split(';'); + changedUsers.newUsers = newUsers.filter((innerUser: string) => !removedUsers.includes(innerUser) && innerUser != ''); + changedUsers.removedUsers = removedUsers.filter((innerUser: string) => !newUsers.includes(innerUser) && innerUser != ''); + user[permission] = newUsers.join(';'); + await localUserRepository.update(user.email, user); + + return changedUsers; +} diff --git a/src/mailcowAPI.ts b/src/mailcowAPI.ts index bdc46c0..d49c390 100644 --- a/src/mailcowAPI.ts +++ b/src/mailcowAPI.ts @@ -1,22 +1,27 @@ import MailCowClient from 'ts-mailcow-api'; -import { ACLEditRequest, MailboxDeleteRequest, MailboxEditRequest, MailboxPostRequest } from 'ts-mailcow-api/src/types'; +import { + ACLEditRequest, + MailboxEditRequest, + MailboxPostRequest, +} from 'ts-mailcow-api/src/types'; import * as https from 'https'; -import { ContainerConfig, UserDataAPI } from './types'; -import { Mailbox, MailboxEditAttributes } from 'ts-mailcow-api/dist/types'; +import { MailcowUserData } from './types'; +import { + Mailbox, + MailboxEditAttributes, +} from 'ts-mailcow-api/dist/types'; +import { containerConfig } from './index'; -// Create MailCowClient based on BASE_URL and API_KEY +const passwordLength: number = 32; let mailcowClient: MailCowClient; -// Set password length -const passwordLength = 32; - /** - * Initialize database connection. Setup database if it does not yet exist + * Initialize database connection */ -export async function initializeMailcowAPI(config: ContainerConfig): Promise { +export async function initializeMailcowAPI(): Promise { mailcowClient = new MailCowClient( - config.API_HOST, - config.API_KEY, + containerConfig.API_HOST, + containerConfig.API_KEY, { httpsAgent: new https.Agent({ keepAlive: true, @@ -30,32 +35,30 @@ export async function initializeMailcowAPI(config: ContainerConfig): Promise { - // Generate password +export async function createMailcowUser(mail: string, name: string, active: number, quotum: number): Promise { const password: string = generatePassword(passwordLength); - // Set details of the net mailbox const mailboxData: MailboxPostRequest = { // Active: 0 = no incoming mail/no login, 1 = allow both, 2 = custom state: allow incoming mail/no login 'active': active, 'force_pw_update': false, - 'local_part': email.split('@')[0], - 'domain': email.split('@')[1], + 'local_part': mail.split('@')[0], + 'domain': mail.split('@')[1], 'name': name, 'quota': quotum, 'password': password, @@ -64,78 +67,52 @@ export async function addUserAPI(email: string, name: string, active: number, qu 'tls_enforce_out': false, }; - // Create mailbox await mailcowClient.mailbox.create(mailboxData); - // Set ACL data of new mailbox const aclData: ACLEditRequest = { - 'items': email, + 'items': mail, 'attr': { 'user_acl': [ 'spam_alias', - //"tls_policy", 'spam_score', 'spam_policy', 'delimiter_action', - // "syncjobs", - // "eas_reset", - // "sogo_profile_reset", 'quarantine', - // "quarantine_attachments", 'quarantine_notification', - // "quarantine_category", - // "app_passwds", - // "pushover" ], }, }; - // Adjust ACL data of new mailbox await mailcowClient.mailbox.editUserACL(aclData); } /** * Edit user in Mailcow - * @param email - email of user to be edited + * @param mail - mail of user to be edited * @param options - options to be edited */ -// Todo add send from ACLs -export async function editUserAPI(email: string, options: Partial): Promise { +export async function editMailcowUser(mail: string, options: Partial): Promise { const mailboxData: MailboxEditRequest = { - 'items': [email], + 'items': [mail], 'attr': options, }; - await mailcowClient.mailbox.edit(mailboxData); -} -/** - * Delete user from Mailcow - * @param email - */ -export async function deleteUserAPI(email: string): Promise { - const mailboxData: MailboxDeleteRequest = { - 'mailboxes': [email], - }; - await mailcowClient.mailbox.delete(mailboxData); + await mailcowClient.mailbox.edit(mailboxData); } /** * Check if user exists in Mailcow - * @param email - email of user + * @param mail - mail of user */ -export async function checkUserAPI(email: string): Promise { - const userData: UserDataAPI = { +export async function getMailcowUser(mail: string): Promise { + const userData: MailcowUserData = { exists: false, isActive: 0, }; - // Get mailbox data from user with email - const mailboxData: Mailbox = (await mailcowClient.mailbox.get(email) - .catch(e => { - throw new Error(e); - }))[0]; + // Should only find one user + const mailboxData: Mailbox = (await mailcowClient.mailbox.get(mail))[0]; - // If no data, return immediately, otherwise return response data if (!(Object.keys(mailboxData).length === 0 && mailboxData.constructor === Object)) { userData.exists = true; userData.isActive = mailboxData.active_int; diff --git a/src/mailcowDB.ts b/src/mailcowDatabase.ts similarity index 55% rename from src/mailcowDB.ts rename to src/mailcowDatabase.ts index ff2eb8e..07bf29c 100644 --- a/src/mailcowDB.ts +++ b/src/mailcowDatabase.ts @@ -1,9 +1,16 @@ -import { DataSource, Repository } from 'typeorm'; -import { ContainerConfig, Defaults, SOGoMailIdentity } from './types'; +import { + DataSource, + Repository, +} from 'typeorm'; +import { + Defaults, + SOGoMailIdentity, +} from './types'; import { SogoUserProfile } from './entities/SogoUserProfile'; import { Users } from './entities/User'; -import { getLDAPDisplayName } from './index'; +import { getActiveDirectoryDisplayName } from './index'; import axios from 'axios'; +import { containerConfig } from './index'; // Connection options for the DB let dataSource : DataSource; @@ -12,13 +19,13 @@ let SogoUserProfileRepository: Repository; /** * Initialize database connection. Setup database if it does not yet exist */ -export async function initializeMailcowDB(config: ContainerConfig): Promise { +export async function initializeMailcowDatabase(): Promise { dataSource = new DataSource({ type: 'mariadb', host: '172.22.1.251', port: 3306, username: 'mailcow', - password: config.DB_PASSWORD, + password: containerConfig.DB_PASSWORD, database: 'mailcow', entities: [ SogoUserProfile, @@ -33,28 +40,32 @@ export async function initializeMailcowDB(config: ContainerConfig): Promise { - if (user.email !== 'm9006@gewis.nl' && user.email !== 'm9093@gewis.nl') return; +/** + * Edit the signatures of a user + * @param user - user to edit the signatures of + * @param SOBs - all SOB for which the user should get signatures + */ +export async function editUserSignatures(user: Users, SOBs: string[]): Promise { console.log(`Changing signatures for ${user.email}`); - let profile = await SogoUserProfileRepository.findOneOrFail({ + let userProfile: SogoUserProfile = await SogoUserProfileRepository.findOneOrFail({ where: { c_uid: user.email, }, }); - let cDefaults : Defaults = JSON.parse(profile.c_defaults); - let newIdentities : SOGoMailIdentity[] = []; + let defaultSettings: Defaults = JSON.parse(userProfile.c_defaults); + let newIdentities: SOGoMailIdentity[] = []; - for (let identity of cDefaults.SOGoMailIdentities) { + for (let identity of defaultSettings.SOGoMailIdentities) { if (identity.signature.indexOf('class="autogenerated"') === -1) { newIdentities.push(identity); } } for (let identityMail of SOBs) { - const committeeDisplayName = await getLDAPDisplayName(identityMail); - let signature : string = (await axios.get(`https://signature.gewis.nl/${identityMail}`)).data; + const committeeDisplayName: string = await getActiveDirectoryDisplayName(identityMail); + let signature: string = (await axios.get(`https://signature.gewis.nl/${identityMail}`)).data; signature = signature.replace('{{displayName}}', user.displayName); signature = signature.replaceAll('{{committeeDisplayName}}', committeeDisplayName); signature = signature.replaceAll('{{identityMail}}', identityMail); @@ -70,8 +81,8 @@ export async function editUserSignature(user: Users, SOBs: string[]): Promise