import { Injectable } from '@angular/core'; import { ClientSettingsService } from '@features/client-settings/client-settings.service'; import { AdaptedExternalCommRecord, Communication } from '@features/communications/communications.typing'; import { ProgramResources } from '@features/programs/program.resources'; import { EmailState } from '@features/system-emails/email.state'; import { ClientEmailTemplateFromAPI, ClientEmailTranslationPayload, CommunicationPrefs, CopyEmailModalResponse, Email, EmailDetail, EmailEditableChunk, EmailMergeModel, EmailNotificationType, EmailSetupPrefs, EmailTemplateCopyForAPI, ProgramEmailTemplateForUI, SimpleEmail, UpdateCommPrefsPayload } from '@features/system-emails/email.typing'; import { ArrayHelpersService, AutoTableRepositoryFactory, ExistingGenericFile, FileUploadRequest, GenericFile, NewGenericFile, SimpleStringMap } from '@yourcause/common'; import { I18nService } from '@yourcause/common/i18n'; import { LogService } from '@yourcause/common/logging'; import { NotifierService } from '@yourcause/common/notifier'; import { AttachYCState, BaseYCService } from '@yourcause/common/state'; import EmailReplyParser from 'email-reply-parser'; import { EmailResources } from './email.resources'; @AttachYCState(EmailState) @Injectable({ providedIn: 'root' }) export class EmailService extends BaseYCService { private placeHolder = ''; emailParser = new EmailReplyParser(); constructor ( private logger: LogService, private emailResources: EmailResources, private arrayHelper: ArrayHelpersService, private autoTableFactory: AutoTableRepositoryFactory, private i18n: I18nService, private notifier: NotifierService, private programResources: ProgramResources, private clientSettingsService: ClientSettingsService ) { super(); } get emails () { return this.get('emails'); } get communicationPrefs () { return this.get('communicationPrefs') || []; } get mergeModelMap () { return this.get('mergeModelMap'); } get templateMap () { return this.get('templateMap'); } get emailTemplateTranslationMap () { return this.get('emailTemplateTranslationMap'); } get emailSubjectMap () { return this.get('emailSubjectMap'); } setEmailsOnState (emails: Email[]) { this.set('emails', emails); } emptyTemplateMap () { this.set('templateMap', {}); } setEmailMergeModelOnState ( emailId: number, mergeModel: EmailMergeModel ) { this.set('mergeModelMap', { ...(this.mergeModelMap || {}), [emailId]: mergeModel }); } setTemplateMapOnState (detail: EmailDetail, type: number) { this.set('templateMap', { ...this.templateMap, [type]: detail }); } setEmailTemplateTranslationMap (langKey: string, template: ClientEmailTemplateFromAPI) { this.set('emailTemplateTranslationMap', { ...this.emailTemplateTranslationMap, [langKey]: template }); } setCommunicationPrefs (prefs: CommunicationPrefs[]) { this.set('communicationPrefs', prefs); } getType (email: Email|ClientEmailTemplateFromAPI) { return (email as Email).emailNotificationType || (email as ClientEmailTemplateFromAPI).emailNotificationTypeId; } setEmailMergeModel ( email: Email ) { this.setEmailMergeModelOnState(email.emailNotificationType, email.mergeModelType); } async setEmails () { let emails = this.emails; if (!emails) { const res = await this.emailResources.getEmailTemplates({ rowsPerPage: 1000, pageNumber: 1, sortColumns: [], filterColumns: [], retrieveTotalRecordCount: false, returnAll: true }); emails = res.records; this.setEmailsOnState(emails); this.setEmailSubjectMap(emails); emails.forEach(email => { this.setEmailMergeModel(email); }); } } setEmailSubjectMap (emails: Email[]) { const map: SimpleStringMap = {}; emails.forEach((email) => { map[email.emailNotificationType] = email.subject; }); this.set('emailSubjectMap', map); } getEmailMergeModel (notificationType: number) { return this.mergeModelMap[notificationType]; } async resetAll ( type: EmailNotificationType, isCopy: boolean, programId?: number ) { if (isCopy) { const map = this.templateMap; this.setTemplateMapOnState({ ...map, [type]: undefined }, type); try { await this.setTemplateMap(type); if (programId) { await this.setProgramTemplatesByType( programId, type ); } } catch (e) { this.logger.error(e); this.notifier.error(this.i18n.translate( 'GLOBAL:textErrorLoadingEmail', {}, 'There was an error loading the copies' )); } } this.setEmailsOnState(undefined); await this.setEmails(); const nomTable = this.autoTableFactory.getRepository('NOMINATION_EMAILS'); const grantTable = this.autoTableFactory.getRepository('PROGRAM_EMAILS'); const allTable = this.autoTableFactory.getRepository('SYSTEM_EMAILS'); if (nomTable) { nomTable.reset(nomTable.pageNumber); } if (grantTable) { grantTable.reset(grantTable.pageNumber); } if (allTable) { allTable.reset(allTable.pageNumber); } } async getProgramDefault (id: number, type: EmailNotificationType) { await this.setTemplateMap(type); await this.setProgramTemplatesByType(id, type); const map = this.templateMap[type]; const templates = map.programTemplates[id]; const found = templates.find((temp) => { return temp.default; }); return found ? found.clientEmailTemplate.id : 0; } getEmailAsEditable (body: string): EmailEditableChunk { const tempEl = document.createElement('table'); tempEl.innerHTML = body; body = tempEl.innerHTML.replace(/\n/g, '\r\n'); const editableElement = tempEl.getElementsByTagName('td')[0]; if (editableElement) { /* replace unix line endings with windows, replace html non breaking space with stupid string */ let editableChunk = editableElement.innerHTML.replace(/\n/g, '\r\n'); editableChunk = editableChunk.replace(/\ \;/g, String.fromCharCode(160)); body = body.replace(/\ \;/g, String.fromCharCode(160)); const nonEditableChunk = body.replace(editableChunk, this.placeHolder); return { nonEditableChunk, editableChunk }; } return { nonEditableChunk: body, editableChunk: body }; } reassembleEditableEmail (chunks: EmailEditableChunk): string { if (chunks.editableChunk.includes('')) { const tempEl = document.createElement('html'); tempEl.innerHTML = chunks.editableChunk; chunks.editableChunk = tempEl.getElementsByTagName('body')[0] .innerHTML.replace(/\n/g, '\r\n'); } return chunks.nonEditableChunk.replace(this.placeHolder, chunks.editableChunk); } async getEmailTokens (type: EmailNotificationType) { const found = this.emails.find((email) => { return email.emailNotificationType === type; }); if (found && found.customizable) { const tokens = await this.emailResources.getTokens(type); return this.arrayHelper.sort(tokens.filter((token) => { const name = token.name.toLowerCase(); return !name.includes('link'); }), 'name'); } return []; } async setTemplateMap ( type: EmailNotificationType, force = false, langId = this.clientSettingsService.defaultLanguage ): Promise { await this.setEmails(); let detail: EmailDetail = this.templateMap[type]; if ( force || ( !detail || !detail.template || !detail.tokens || !detail.clientTemplates ) ) { const [ template, tokens, clientTemplates ] = await Promise.all([ this.emailResources.getEmailTemplate(type), this.getEmailTokens(type), this.emailResources.getClientTemplatesForEmail(type, langId) ]); const email = this.emails.find((item) => { return item.emailNotificationType === type; }); detail = { template: template.template, tokens, email, clientTemplates: ((clientTemplates || [])).map((temp) => { return { ...temp, translationMap: { [langId]: { ...temp } } }; }) }; this.setTemplateMapOnState(detail, type); } return detail; } async setProgramTemplatesByType ( programId: number, type: EmailNotificationType ) { const detail: EmailDetail = this.templateMap[type]; if ( !detail || !detail.programTemplates || !detail.programTemplates[programId] ) { const adaptedProgramTemplates = await this.getAdaptedProgramTempsByEmailType( programId, type ); this.setTemplateMapOnState({ ...detail, programTemplates: { ...detail.programTemplates || {}, [programId]: this.arrayHelper.sort(adaptedProgramTemplates || [], 'id') } }, type); } } async getAdaptedProgramTempsByEmailType ( programId: number, type: EmailNotificationType ): Promise { const programTemplatesFromApi = await this.emailResources.getProgramTemplatesByEmailType( programId, type ); const adaptedProgramTemplates = programTemplatesFromApi.map((temp) => { return { ...temp, emailNumber: temp.clientEmailTemplate.emailNumber, clientEmailTemplate: { ...temp.clientEmailTemplate, attachments: temp.clientEmailTemplate.emailAttachments } }; }); return adaptedProgramTemplates; } async toggleActivateClientEmail (id: number, activate = true) { if (activate) { await this.emailResources.activateClientTemplate(id); } else { await this.emailResources.deactivateClientTemplate(id); } } isEmailActive (type: EmailNotificationType) { const found = this.getEmailByType(type); if (found) { return !found.isDisabled; } return true; } async isProgramEmailActive (type: EmailNotificationType, programId: number) { await this.setEmails(); const response = await this.programResources.getProgramEmailSettings( programId ); const found = response.disabledEmails.find((item) => { return item.id === type; }); return found ? false : this.isEmailActive(type); } getEmailByType (type: EmailNotificationType) { return (this.emails || []).find((email) => { return +email.emailNotificationType === +type; }); } async getCommunicationPrefs () { const prefs = await this.emailResources.getCommunicationPrefs(); this.setCommunicationPrefs(prefs || []); } async updateCommunicationsPrefs ( payload: UpdateCommPrefsPayload, email: EmailSetupPrefs ) { try { await this.emailResources.updateCommunicationPrefs(payload); this.notifier.success(this.i18n.translate( 'ACCOUNT:textSuccessfullyUpdatedCommunicationPrefs', {}, 'Successfully updated communication preferences' )); const index = this.communicationPrefs.findIndex((pref) => { return pref.emailNotificationType === email.type; }); let updatedPrefs; if (index > -1) { updatedPrefs = [ ...this.communicationPrefs.slice(0, index), payload, ...this.communicationPrefs.slice(index + 1) ]; } else { updatedPrefs = [ ...this.communicationPrefs, payload ]; } this.setCommunicationPrefs(updatedPrefs); } catch (e) { this.notifier.error(this.i18n.translate( 'ACCOUNT:textErrorUpdatedCommunicationPrefs', {}, 'There was an error updating communication preferences' )); } } async handleAttachmentToEmail ( file: NewGenericFile, applicationId?: number, clientEmailTemplateId?: number ) { const fileId = await this.emailResources.addAttachmentToEmailTemplate( file.file, applicationId, clientEmailTemplateId ); return fileId; } async returnIdsFromMixedAttachments ( attachments: GenericFile[], applicationId?: number, // pass for one off email sends for an application clientEmailTemplateId?: number // pass for system email attachment changes ): Promise { attachments = attachments || []; const attachmentArray: number[] = attachments.filter((att) => { return ('fileUploadId' in att); }).map((att) => (att as ExistingGenericFile).fileUploadId); // Grab the new attachments for uploading await Promise.all(attachments.map(async (gFile: GenericFile) => { if (!('fileUploadId' in gFile)) { // Upload new attachments and return ID const attachmentId = await this.handleAttachmentToEmail( gFile, applicationId, clientEmailTemplateId ); attachmentArray.push(+attachmentId); } })); return attachmentArray; } async handleBulkAddTranslationToClientEmail ( email: SimpleEmail, copyModalReturn: CopyEmailModalResponse ) { try { const payloadArray: ClientEmailTranslationPayload[] = []; copyModalReturn.templates.forEach((translation) => { const payload = { subjectText: translation.subject, titleText: translation.title, bodyText: translation.body, id: email.id, languageId: translation.languageId }; payloadArray.push(payload); }); const bulkTranslationPayload = { clientEmailTemplateId: email.id, translations: payloadArray }; await this.emailResources.bulkAddTranslationToClientEmail(bulkTranslationPayload); } catch (e) { this.logger.error(e); this.notifier.error(this.i18n.translate( 'GLOBAL:textErrorTranslatingEmailCopy', {}, 'There was an error translating the email copy' )); } } async handleCreateOrUpdateCopy ( copyPayload: EmailTemplateCopyForAPI ) { const isEdit = !!copyPayload.id; try { const id = this.emailResources.createCopy(copyPayload); this.notifier.success( this.i18n.translate( 'ACCOUNT:textSuccessfullyUpdatedEmailCopy', {}, 'Successfully updated email copy' ) ); return id; } catch (e) { this.logger.error(e); this.notifier.error(this.i18n.translate( isEdit ? 'GLOBAL:textErrorUpdatingEmailCopy' : 'GLOBAL:textErrorCretingEmailCopy', {}, isEdit ? 'There was an error updating the email copy' : 'There was an error creating the email copy' )); return null; } } async getUpdatedAttachments ( type: EmailNotificationType, clientEmailTemplateId: number, langId = this.clientSettingsService.defaultLanguage ): Promise { const templates = await this.emailResources.getClientTemplatesForEmail(type, langId); const found = templates.find((temp) => { return temp.id === clientEmailTemplateId; }); return found.emailAttachments; } getShowCheckboxForCcEdit (type: EmailNotificationType) { // these don't have user input, so no need to allow CC/BCC edits return ![ EmailNotificationType.ApplicationSubmittedConfirmation, EmailNotificationType.NominationSubmittedConfirmation, EmailNotificationType.AppAdditionalFormSubmittedApplicant, EmailNotificationType.AppAdditionalFormSubmittedManager, EmailNotificationType.NomAdditionalFormSubmittedApplicant, EmailNotificationType.NomAdditionalFormSubmittedManager, EmailNotificationType.VettingDeclinedApplicant, EmailNotificationType.VettingApprovedApplicant, EmailNotificationType.VettingApprovedManager, EmailNotificationType.VettingDeclinedManager, EmailNotificationType.PaymentFulfilled, EmailNotificationType.ProgramClosingReminderForApplicant, EmailNotificationType.ProgramClosingReminderForNominator ].includes(type); } getClientTranslatedTemplate ( langId: string, emailId: number ) { return this.emailResources.getClientTranslatedTemplate(langId, emailId); } async uploadEmailImage (fileUploadRequest: FileUploadRequest) { try { const response = await this.emailResources.uploadEmailImage(fileUploadRequest.file); this.notifier.success( this.i18n.translate( 'common:textSuccessfullyUploadedImage', {}, 'Successfully uploaded image' ) ); return response; } catch (e) { this.logger.error(e); this.notifier.error( this.i18n.translate( 'common:textErrorUploadingImage', {}, 'There was an error uploading the image' ) ); return null; } } getEmailFragmentsFromString (contentString: string) { return this.emailParser.read(contentString); } getAdaptedCommRecord ( commRecord: Communication ): AdaptedExternalCommRecord { const parsedEmail = this.getEmailFragmentsFromString(commRecord.content); const emailFragments = parsedEmail.fragments; const latestComm = emailFragments[0]; const previousCommsThread = emailFragments.filter( ( fragment: EmailReplyParser.Fragment, index: number ) => { return index!== 0 && !!fragment.content && fragment.content !== '>' && fragment.content.length > 1; } ); return { ...commRecord, content: emailFragments, latestComm, previousCommsThread }; } }