[NEW] Email Inboxes for Omnichannel (#20101)
parent
64d77c99b5
commit
2f90da3f17
@ -0,0 +1,79 @@ |
||||
import { EmailInbox } from '../../../models/server/raw'; |
||||
import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; |
||||
import { Users } from '../../../models'; |
||||
|
||||
export async function findEmailInboxes({ userId, query = {}, pagination: { offset, count, sort } }) { |
||||
if (!await hasPermissionAsync(userId, 'manage-email-inbox')) { |
||||
throw new Error('error-not-allowed'); |
||||
} |
||||
const cursor = EmailInbox.find(query, { |
||||
sort: sort || { name: 1 }, |
||||
skip: offset, |
||||
limit: count, |
||||
}); |
||||
|
||||
const total = await cursor.count(); |
||||
|
||||
const emailInboxes = await cursor.toArray(); |
||||
|
||||
return { |
||||
emailInboxes, |
||||
count: emailInboxes.length, |
||||
offset, |
||||
total, |
||||
}; |
||||
} |
||||
|
||||
export async function findOneEmailInbox({ userId, _id }) { |
||||
if (!await hasPermissionAsync(userId, 'manage-email-inbox')) { |
||||
throw new Error('error-not-allowed'); |
||||
} |
||||
return EmailInbox.findOneById(_id); |
||||
} |
||||
|
||||
export async function insertOneOrUpdateEmailInbox(userId, emailInboxParams) { |
||||
const { _id, active, name, email, description, senderInfo, department, smtp, imap } = emailInboxParams; |
||||
|
||||
if (!_id) { |
||||
emailInboxParams._createdAt = new Date(); |
||||
emailInboxParams._updatedAt = new Date(); |
||||
emailInboxParams._createdBy = Users.findOne(userId, { fields: { username: 1 } }); |
||||
return EmailInbox.insertOne(emailInboxParams); |
||||
} |
||||
|
||||
const emailInbox = await findOneEmailInbox({ userId, id: _id }); |
||||
|
||||
if (!emailInbox) { |
||||
throw new Error('error-invalid-email-inbox'); |
||||
} |
||||
|
||||
const updateEmailInbox = { |
||||
$set: { |
||||
active, |
||||
name, |
||||
email, |
||||
description, |
||||
senderInfo, |
||||
smtp, |
||||
imap, |
||||
_updatedAt: new Date(), |
||||
}, |
||||
}; |
||||
|
||||
if (department === 'All') { |
||||
updateEmailInbox.$unset = { |
||||
department: 1, |
||||
}; |
||||
} else { |
||||
updateEmailInbox.$set.department = department; |
||||
} |
||||
|
||||
return EmailInbox.updateOne({ _id }, updateEmailInbox); |
||||
} |
||||
|
||||
export async function findOneEmailInboxByEmail({ userId, email }) { |
||||
if (!await hasPermissionAsync(userId, 'manage-email-inbox')) { |
||||
throw new Error('error-not-allowed'); |
||||
} |
||||
return EmailInbox.findOne({ email }); |
||||
} |
||||
@ -0,0 +1,131 @@ |
||||
import { check, Match } from 'meteor/check'; |
||||
|
||||
import { API } from '../api'; |
||||
import { findEmailInboxes, findOneEmailInbox, insertOneOrUpdateEmailInbox } from '../lib/emailInbox'; |
||||
import { hasPermission } from '../../../authorization/server/functions/hasPermission'; |
||||
import { EmailInbox } from '../../../models'; |
||||
import Users from '../../../models/server/models/Users'; |
||||
import { sendTestEmailToInbox } from '../../../../server/features/EmailInbox/EmailInbox_Outgoing'; |
||||
|
||||
API.v1.addRoute('email-inbox.list', { authRequired: true }, { |
||||
get() { |
||||
const { offset, count } = this.getPaginationItems(); |
||||
const { sort, query } = this.parseJsonQuery(); |
||||
const emailInboxes = Promise.await(findEmailInboxes({ userId: this.userId, query, pagination: { offset, count, sort } })); |
||||
|
||||
return API.v1.success(emailInboxes); |
||||
}, |
||||
}); |
||||
|
||||
API.v1.addRoute('email-inbox', { authRequired: true }, { |
||||
post() { |
||||
if (!hasPermission(this.userId, 'manage-email-inbox')) { |
||||
throw new Error('error-not-allowed'); |
||||
} |
||||
check(this.bodyParams, { |
||||
_id: Match.Maybe(String), |
||||
name: String, |
||||
email: String, |
||||
active: Boolean, |
||||
description: Match.Maybe(String), |
||||
senderInfo: Match.Maybe(String), |
||||
department: Match.Maybe(String), |
||||
smtp: Match.ObjectIncluding({ |
||||
password: String, |
||||
port: Number, |
||||
secure: Boolean, |
||||
server: String, |
||||
username: String, |
||||
}), |
||||
imap: Match.ObjectIncluding({ |
||||
password: String, |
||||
port: Number, |
||||
secure: Boolean, |
||||
server: String, |
||||
username: String, |
||||
}), |
||||
}); |
||||
|
||||
const emailInboxParams = this.bodyParams; |
||||
|
||||
const { _id } = emailInboxParams; |
||||
|
||||
Promise.await(insertOneOrUpdateEmailInbox(this.userId, emailInboxParams)); |
||||
|
||||
return API.v1.success({ _id }); |
||||
}, |
||||
}); |
||||
|
||||
API.v1.addRoute('email-inbox/:_id', { authRequired: true }, { |
||||
get() { |
||||
check(this.urlParams, { |
||||
_id: String, |
||||
}); |
||||
|
||||
const { _id } = this.urlParams; |
||||
if (!_id) { throw new Error('error-invalid-param'); } |
||||
const emailInboxes = Promise.await(findOneEmailInbox({ userId: this.userId, _id })); |
||||
|
||||
return API.v1.success(emailInboxes); |
||||
}, |
||||
delete() { |
||||
if (!hasPermission(this.userId, 'manage-email-inbox')) { |
||||
throw new Error('error-not-allowed'); |
||||
} |
||||
check(this.urlParams, { |
||||
_id: String, |
||||
}); |
||||
|
||||
const { _id } = this.urlParams; |
||||
if (!_id) { throw new Error('error-invalid-param'); } |
||||
|
||||
const emailInboxes = EmailInbox.findOneById(_id); |
||||
|
||||
if (!emailInboxes) { |
||||
return API.v1.notFound(); |
||||
} |
||||
EmailInbox.removeById(_id); |
||||
return API.v1.success({ _id }); |
||||
}, |
||||
}); |
||||
|
||||
API.v1.addRoute('email-inbox.search', { authRequired: true }, { |
||||
get() { |
||||
if (!hasPermission(this.userId, 'manage-email-inbox')) { |
||||
throw new Error('error-not-allowed'); |
||||
} |
||||
check(this.queryParams, { |
||||
email: String, |
||||
}); |
||||
|
||||
const { email } = this.queryParams; |
||||
const emailInbox = Promise.await(EmailInbox.findOne({ email })); |
||||
|
||||
return API.v1.success({ emailInbox }); |
||||
}, |
||||
}); |
||||
|
||||
API.v1.addRoute('email-inbox.send-test/:_id', { authRequired: true }, { |
||||
post() { |
||||
if (!hasPermission(this.userId, 'manage-email-inbox')) { |
||||
throw new Error('error-not-allowed'); |
||||
} |
||||
check(this.urlParams, { |
||||
_id: String, |
||||
}); |
||||
|
||||
const { _id } = this.urlParams; |
||||
if (!_id) { throw new Error('error-invalid-param'); } |
||||
const emailInbox = Promise.await(findOneEmailInbox({ userId: this.userId, _id })); |
||||
|
||||
if (!emailInbox) { |
||||
return API.v1.notFound(); |
||||
} |
||||
|
||||
const user = Users.findOneById(this.userId); |
||||
|
||||
Promise.await(sendTestEmailToInbox(emailInbox, user)); |
||||
|
||||
return API.v1.success({ _id }); |
||||
}, |
||||
}); |
||||
@ -0,0 +1,27 @@ |
||||
import { Base } from './_Base'; |
||||
|
||||
export class EmailInbox extends Base { |
||||
constructor() { |
||||
super('email_inbox'); |
||||
|
||||
this.tryEnsureIndex({ email: 1 }, { unique: true }); |
||||
} |
||||
|
||||
findOneById(_id, options) { |
||||
return this.findOne(_id, options); |
||||
} |
||||
|
||||
create(data) { |
||||
return this.insert(data); |
||||
} |
||||
|
||||
updateById(_id, data) { |
||||
return this.update({ _id }, data); |
||||
} |
||||
|
||||
removeById(_id) { |
||||
return this.remove(_id); |
||||
} |
||||
} |
||||
|
||||
export default new EmailInbox(); |
||||
@ -0,0 +1,6 @@ |
||||
import { BaseRaw } from './BaseRaw'; |
||||
import { IEmailInbox } from '../../../../definition/IEmailInbox'; |
||||
|
||||
export class EmailInboxRaw extends BaseRaw<IEmailInbox> { |
||||
//
|
||||
} |
||||
@ -0,0 +1,3 @@ |
||||
export declare const slashCommand: { |
||||
add(command: string, callback: Function, options: object /* , result, providesPreview = false, previewer, previewCallback*/): void; |
||||
}; |
||||
@ -0,0 +1,361 @@ |
||||
import React, { useCallback, useState } from 'react'; |
||||
import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; |
||||
import { |
||||
Accordion, |
||||
Button, |
||||
ButtonGroup, |
||||
TextInput, |
||||
TextAreaInput, |
||||
Field, |
||||
ToggleSwitch, |
||||
FieldGroup, |
||||
Box, |
||||
Margins, |
||||
} from '@rocket.chat/fuselage'; |
||||
|
||||
import { AutoCompleteDepartment } from '../../../components/AutoCompleteDepartment'; |
||||
import { useTranslation } from '../../../contexts/TranslationContext'; |
||||
import { useRoute } from '../../../contexts/RouterContext'; |
||||
import { useToastMessageDispatch } from '../../../contexts/ToastMessagesContext'; |
||||
import Page from '../../../components/Page'; |
||||
import { useForm } from '../../../hooks/useForm'; |
||||
import { useEndpointAction } from '../../../hooks/useEndpointAction'; |
||||
import { isEmail } from '../../../../app/utils'; |
||||
import { useEndpointData } from '../../../hooks/useEndpointData'; |
||||
import { AsyncStatePhase } from '../../../hooks/useAsyncState'; |
||||
import { FormSkeleton } from './Skeleton'; |
||||
import DeleteWarningModal from '../../../components/DeleteWarningModal'; |
||||
import { useSetModal } from '../../../contexts/ModalContext'; |
||||
import { useComponentDidUpdate } from '../../../hooks/useComponentDidUpdate'; |
||||
|
||||
|
||||
const initialValues = { |
||||
active: true, |
||||
name: '', |
||||
email: '', |
||||
description: '', |
||||
senderInfo: '', |
||||
department: '', |
||||
// SMTP
|
||||
smtpServer: '', |
||||
smtpPort: 587, |
||||
smtpUsername: '', |
||||
smtpPassword: '', |
||||
smtpSecure: false, |
||||
// IMAP
|
||||
imapServer: '', |
||||
imapPort: 993, |
||||
imapUsername: '', |
||||
imapPassword: '', |
||||
imapSecure: false, |
||||
}; |
||||
|
||||
const getInitialValues = (data) => { |
||||
if (!data) { |
||||
return initialValues; |
||||
} |
||||
|
||||
const { |
||||
active, |
||||
name, |
||||
email, |
||||
description, |
||||
senderInfo, |
||||
department, |
||||
smtp, |
||||
imap, |
||||
} = data; |
||||
|
||||
return { |
||||
active: active ?? true, |
||||
name: name ?? '', |
||||
email: email ?? '', |
||||
description: description ?? '', |
||||
senderInfo: senderInfo ?? '', |
||||
department: department ?? '', |
||||
// SMTP
|
||||
smtpServer: smtp.server ?? '', |
||||
smtpPort: smtp.port ?? 587, |
||||
smtpUsername: smtp.username ?? '', |
||||
smtpPassword: smtp.password ?? '', |
||||
smtpSecure: smtp.secure ?? false, |
||||
// IMAP
|
||||
imapServer: imap.server ?? '', |
||||
imapPort: imap.port ?? 993, |
||||
imapUsername: imap.username ?? '', |
||||
imapPassword: imap.password ?? '', |
||||
imapSecure: imap.secure ?? false, |
||||
}; |
||||
}; |
||||
|
||||
export function EmailInboxEditWithData({ id }) { |
||||
const t = useTranslation(); |
||||
const { value: data, error, phase: state } = useEndpointData(`email-inbox/${ id }`); |
||||
|
||||
if ([state].includes(AsyncStatePhase.LOADING)) { |
||||
return <FormSkeleton/>; |
||||
} |
||||
|
||||
if (error || !data) { |
||||
return <Box mbs='x16'>{t('EmailInbox_not_found')}</Box>; |
||||
} |
||||
|
||||
return <EmailInboxForm id={id} data={data} />; |
||||
} |
||||
|
||||
export default function EmailInboxForm({ id, data }) { |
||||
const t = useTranslation(); |
||||
const dispatchToastMessage = useToastMessageDispatch(); |
||||
const setModal = useSetModal(); |
||||
const [emailError, setEmailError] = useState(); |
||||
const { values, handlers, hasUnsavedChanges } = useForm(getInitialValues(data)); |
||||
|
||||
const { |
||||
handleActive, |
||||
handleName, |
||||
handleEmail, |
||||
handleDescription, |
||||
handleSenderInfo, |
||||
handleDepartment, |
||||
// SMTP
|
||||
handleSmtpServer, |
||||
handleSmtpPort, |
||||
handleSmtpUsername, |
||||
handleSmtpPassword, |
||||
handleSmtpSecure, |
||||
// IMAP
|
||||
handleImapServer, |
||||
handleImapPort, |
||||
handleImapUsername, |
||||
handleImapPassword, |
||||
handleImapSecure, |
||||
} = handlers; |
||||
const { |
||||
active, |
||||
name, |
||||
email, |
||||
description, |
||||
senderInfo, |
||||
department, |
||||
// SMTP
|
||||
smtpServer, |
||||
smtpPort, |
||||
smtpUsername, |
||||
smtpPassword, |
||||
smtpSecure, |
||||
// IMAP
|
||||
imapServer, |
||||
imapPort, |
||||
imapUsername, |
||||
imapPassword, |
||||
imapSecure, |
||||
} = values; |
||||
|
||||
const router = useRoute('admin-email-inboxes'); |
||||
|
||||
const close = useCallback(() => router.push({}), [router]); |
||||
|
||||
const saveEmailInbox = useEndpointAction('POST', 'email-inbox'); |
||||
const deleteAction = useEndpointAction('DELETE', `email-inbox/${ id }`); |
||||
const emailAlreadyExistsAction = useEndpointAction('GET', `email-inbox.search?email=${ email }`); |
||||
|
||||
useComponentDidUpdate(() => { |
||||
setEmailError(!isEmail(email) ? t('Validate_email_address') : null); |
||||
}, [t, email]); |
||||
useComponentDidUpdate(() => { |
||||
!email && setEmailError(null); |
||||
}, [email]); |
||||
|
||||
const handleRemoveClick = useMutableCallback(async () => { |
||||
const result = await deleteAction(); |
||||
if (result.success === true) { |
||||
close(); |
||||
} |
||||
}); |
||||
|
||||
const handleDelete = useMutableCallback((e) => { |
||||
e.stopPropagation(); |
||||
const onDeleteManager = async () => { |
||||
try { |
||||
await handleRemoveClick(); |
||||
dispatchToastMessage({ type: 'success', message: t('Removed') }); |
||||
} catch (error) { |
||||
dispatchToastMessage({ type: 'error', message: error }); |
||||
} |
||||
setModal(); |
||||
}; |
||||
|
||||
setModal(<DeleteWarningModal |
||||
onDelete={onDeleteManager} |
||||
onCancel={() => setModal()} |
||||
>{t('You_will_not_be_able_to_recover_email_inbox')}</DeleteWarningModal>); |
||||
}); |
||||
|
||||
const handleSave = useMutableCallback(async () => { |
||||
const smtp = { server: smtpServer, port: parseInt(smtpPort), username: smtpUsername, password: smtpPassword, secure: smtpSecure }; |
||||
const imap = { server: imapServer, port: parseInt(imapPort), username: imapUsername, password: imapPassword, secure: imapSecure }; |
||||
const payload = { active, name, email, description, senderInfo, department, smtp, imap }; |
||||
if (id) { |
||||
payload._id = id; |
||||
} |
||||
try { |
||||
await saveEmailInbox(payload); |
||||
dispatchToastMessage({ type: 'success', message: t('Saved') }); |
||||
close(); |
||||
} catch (e) { |
||||
dispatchToastMessage({ type: 'error', message: e }); |
||||
} |
||||
}); |
||||
|
||||
|
||||
const checkEmailExists = useMutableCallback(async () => { |
||||
if (!email && !isEmail(email)) { return; } |
||||
const { emailInbox } = await emailAlreadyExistsAction(); |
||||
|
||||
if (!emailInbox || (id && emailInbox._id === id)) { return; } |
||||
setEmailError(t('Email_already_exists')); |
||||
}); |
||||
|
||||
const canSave = hasUnsavedChanges && name && (email && isEmail(email) && !emailError) |
||||
&& smtpServer && smtpPort && smtpUsername && smtpPassword |
||||
&& imapServer && imapPort && imapUsername && imapPassword; |
||||
|
||||
return <Page.ScrollableContentWithShadow> |
||||
<Box maxWidth='x600' w='full' alignSelf='center'> |
||||
<Accordion> |
||||
<Accordion.Item defaultExpanded title={t('Inbox_Info')}> |
||||
<FieldGroup> |
||||
<Field> |
||||
<Field.Label display='flex' justifyContent='space-between' w='full'> |
||||
{t('Active')} |
||||
<ToggleSwitch checked={active} onChange={handleActive}/> |
||||
</Field.Label> |
||||
</Field> |
||||
<Field> |
||||
<Field.Label>{t('Name')}*</Field.Label> |
||||
<Field.Row> |
||||
<TextInput value={name} onChange={handleName} /> |
||||
</Field.Row> |
||||
</Field> |
||||
<Field> |
||||
<Field.Label>{t('Email')}*</Field.Label> |
||||
<Field.Row> |
||||
<TextInput onBlur={checkEmailExists} error={emailError} value={email} onChange={handleEmail} /> |
||||
</Field.Row> |
||||
<Field.Error> |
||||
{t(emailError)} |
||||
</Field.Error> |
||||
</Field> |
||||
<Field> |
||||
<Field.Label>{t('Description')}</Field.Label> |
||||
<Field.Row> |
||||
<TextAreaInput value={description} rows={4} onChange={handleDescription} /> |
||||
</Field.Row> |
||||
</Field> |
||||
<Field> |
||||
<Field.Label>{t('Sender_Info')}</Field.Label> |
||||
<Field.Row> |
||||
<TextInput value={senderInfo} onChange={handleSenderInfo} placeholder={t('Optional')} /> |
||||
</Field.Row> |
||||
<Field.Hint> |
||||
{t('Will_Appear_In_From')} |
||||
</Field.Hint> |
||||
</Field> |
||||
<Field> |
||||
<Field.Label>{t('Department')}</Field.Label> |
||||
<Field.Row> |
||||
<AutoCompleteDepartment value={department} onChange={handleDepartment} /> |
||||
</Field.Row> |
||||
<Field.Hint> |
||||
{t('Only_Members_Selected_Department_Can_View_Channel')} |
||||
</Field.Hint> |
||||
</Field> |
||||
</FieldGroup> |
||||
</Accordion.Item> |
||||
<Accordion.Item title={t('Configure_Outgoing_Mail_SMTP')}> |
||||
<FieldGroup> |
||||
<Field> |
||||
<Field.Label>{t('Server')}*</Field.Label> |
||||
<Field.Row> |
||||
<TextInput value={smtpServer} onChange={handleSmtpServer} /> |
||||
</Field.Row> |
||||
</Field> |
||||
<Field> |
||||
<Field.Label>{t('Port')}*</Field.Label> |
||||
<Field.Row> |
||||
<TextInput type='number' value={smtpPort} onChange={handleSmtpPort} /> |
||||
</Field.Row> |
||||
</Field> |
||||
<Field> |
||||
<Field.Label>{t('Username')}*</Field.Label> |
||||
<Field.Row> |
||||
<TextInput value={smtpUsername} onChange={handleSmtpUsername} /> |
||||
</Field.Row> |
||||
</Field> |
||||
<Field> |
||||
<Field.Label>{t('Password')}*</Field.Label> |
||||
<Field.Row> |
||||
<TextInput type='password' value={smtpPassword} onChange={handleSmtpPassword} /> |
||||
</Field.Row> |
||||
</Field> |
||||
<Field> |
||||
<Field.Label display='flex' justifyContent='space-between' w='full'> |
||||
{t('Connect_SSL_TLS')} |
||||
<ToggleSwitch checked={smtpSecure} onChange={handleSmtpSecure}/> |
||||
</Field.Label> |
||||
</Field> |
||||
</FieldGroup> |
||||
</Accordion.Item> |
||||
<Accordion.Item title={t('Configure_Incoming_Mail_IMAP')}> |
||||
<FieldGroup> |
||||
<Field> |
||||
<Field.Label>{t('Server')}*</Field.Label> |
||||
<Field.Row> |
||||
<TextInput value={imapServer} onChange={handleImapServer} /> |
||||
</Field.Row> |
||||
</Field> |
||||
<Field> |
||||
<Field.Label>{t('Port')}*</Field.Label> |
||||
<Field.Row> |
||||
<TextInput type='number' value={imapPort} onChange={handleImapPort} /> |
||||
</Field.Row> |
||||
</Field> |
||||
<Field> |
||||
<Field.Label>{t('Username')}*</Field.Label> |
||||
<Field.Row> |
||||
<TextInput value={imapUsername} onChange={handleImapUsername}/> |
||||
</Field.Row> |
||||
</Field> |
||||
<Field> |
||||
<Field.Label>{t('Password')}*</Field.Label> |
||||
<Field.Row> |
||||
<TextInput type='password' value={imapPassword} onChange={handleImapPassword} /> |
||||
</Field.Row> |
||||
</Field> |
||||
<Field> |
||||
<Field.Label display='flex' justifyContent='space-between' w='full'> |
||||
{t('Connect_SSL_TLS')} |
||||
<ToggleSwitch checked={imapSecure} onChange={handleImapSecure} /> |
||||
</Field.Label> |
||||
</Field> |
||||
</FieldGroup> |
||||
</Accordion.Item> |
||||
<Field> |
||||
<Field.Row> |
||||
<ButtonGroup stretch w='full'> |
||||
<Button onClick={close}>{t('Cancel')}</Button> |
||||
<Button disabled={!canSave} primary onClick={handleSave}>{t('Save')}</Button> |
||||
</ButtonGroup> |
||||
</Field.Row> |
||||
<Field.Row> |
||||
<Margins blockStart='x16'> |
||||
<ButtonGroup stretch w='full'> |
||||
{id && <Button primary danger onClick={handleDelete}>{t('Delete')}</Button>} |
||||
</ButtonGroup> |
||||
</Margins> |
||||
</Field.Row> |
||||
</Field> |
||||
</Accordion> |
||||
</Box> |
||||
</Page.ScrollableContentWithShadow>; |
||||
} |
||||
@ -0,0 +1,42 @@ |
||||
import React from 'react'; |
||||
import { Button, Icon } from '@rocket.chat/fuselage'; |
||||
|
||||
import Page from '../../../components/Page'; |
||||
import { useTranslation } from '../../../contexts/TranslationContext'; |
||||
import { useRoute, useRouteParameter } from '../../../contexts/RouterContext'; |
||||
import EmailInboxTable from './EmailInboxTable'; |
||||
import EmailInboxForm, { EmailInboxEditWithData } from './EmailInboxForm'; |
||||
|
||||
|
||||
export function EmailInboxPage() { |
||||
const t = useTranslation(); |
||||
|
||||
const context = useRouteParameter('context'); |
||||
const id = useRouteParameter('_id'); |
||||
|
||||
const emailInboxRoute = useRoute('admin-email-inboxes'); |
||||
|
||||
const handleNewButtonClick = () => { |
||||
emailInboxRoute.push({ context: 'new' }); |
||||
}; |
||||
|
||||
return <Page flexDirection='row'> |
||||
<Page> |
||||
<Page.Header title={t('Email_Inboxes')}> |
||||
{context && <Button alignSelf='flex-end' onClick={() => emailInboxRoute.push({})}> |
||||
<Icon name='back'/>{t('Back')} |
||||
</Button>} |
||||
{!context && <Button primary onClick={handleNewButtonClick}> |
||||
<Icon name='plus'/> {t('New_Email_Inbox')} |
||||
</Button>} |
||||
</Page.Header> |
||||
<Page.Content> |
||||
{!context && <EmailInboxTable />} |
||||
{context === 'new' && <EmailInboxForm />} |
||||
{context === 'edit' && <EmailInboxEditWithData id={id} />} |
||||
</Page.Content> |
||||
</Page> |
||||
</Page>; |
||||
} |
||||
|
||||
export default EmailInboxPage; |
||||
@ -0,0 +1,17 @@ |
||||
import React from 'react'; |
||||
|
||||
import { usePermission } from '../../../contexts/AuthorizationContext'; |
||||
import NotAuthorizedPage from '../../../components/NotAuthorizedPage'; |
||||
import EmailInboxPage from './EmailInboxPage'; |
||||
|
||||
function EmailInboxRoute() { |
||||
const canViewEmailInbox = usePermission('manage-email-inbox'); |
||||
|
||||
if (!canViewEmailInbox) { |
||||
return <NotAuthorizedPage />; |
||||
} |
||||
|
||||
return <EmailInboxPage />; |
||||
} |
||||
|
||||
export default EmailInboxRoute; |
||||
@ -0,0 +1,73 @@ |
||||
import { Button, Table, Icon } from '@rocket.chat/fuselage'; |
||||
import React, { useMemo, useCallback, useState } from 'react'; |
||||
import { useDebouncedValue } from '@rocket.chat/fuselage-hooks'; |
||||
|
||||
import GenericTable from '../../../components/GenericTable'; |
||||
import { useTranslation } from '../../../contexts/TranslationContext'; |
||||
import { useRoute } from '../../../contexts/RouterContext'; |
||||
import { useEndpointData } from '../../../hooks/useEndpointData'; |
||||
import { useEndpoint } from '../../../contexts/ServerContext'; |
||||
import { useToastMessageDispatch } from '../../../contexts/ToastMessagesContext'; |
||||
|
||||
export function SendTestButton({ id }) { |
||||
const t = useTranslation(); |
||||
|
||||
const dispatchToastMessage = useToastMessageDispatch(); |
||||
const sendTest = useEndpoint('POST', `email-inbox.send-test/${ id }`); |
||||
|
||||
return <Table.Cell fontScale='p1' color='hint' withTruncatedText> |
||||
<Button small ghost title={t('Send_Test_Email')} onClick={(e) => e.preventDefault() & e.stopPropagation() & sendTest() & dispatchToastMessage({ type: 'success', message: t('Email_sent') })}> |
||||
<Icon name='send' size='x20'/> |
||||
</Button> |
||||
</Table.Cell>; |
||||
} |
||||
|
||||
const useQuery = ({ itemsPerPage, current }, [column, direction]) => useMemo(() => ({ |
||||
sort: JSON.stringify({ [column]: direction === 'asc' ? 1 : -1 }), |
||||
...itemsPerPage && { count: itemsPerPage }, |
||||
...current && { offset: current }, |
||||
}), [column, current, direction, itemsPerPage]); |
||||
|
||||
function EmailInboxTable() { |
||||
const t = useTranslation(); |
||||
|
||||
const [params, setParams] = useState({ current: 0, itemsPerPage: 25 }); |
||||
const [sort] = useState(['name', 'asc']); |
||||
const debouncedParams = useDebouncedValue(params, 500); |
||||
const debouncedSort = useDebouncedValue(sort, 500); |
||||
const query = useQuery(debouncedParams, debouncedSort); |
||||
const router = useRoute('admin-email-inboxes'); |
||||
|
||||
const onClick = useCallback((_id) => () => router.push({ |
||||
context: 'edit', |
||||
_id, |
||||
}), [router]); |
||||
|
||||
|
||||
const header = useMemo(() => [ |
||||
<GenericTable.HeaderCell key={'name'} direction={sort[1]} active={sort[0] === 'name'}>{t('Name')}</GenericTable.HeaderCell>, |
||||
<GenericTable.HeaderCell key={'email'} direction={sort[1]} active={sort[0] === 'email'}>{t('Email')}</GenericTable.HeaderCell>, |
||||
<GenericTable.HeaderCell key={'active'} direction={sort[1]} active={sort[0] === 'active'}>{t('Active')}</GenericTable.HeaderCell>, |
||||
<GenericTable.HeaderCell key={'sendTest'} w='x60'></GenericTable.HeaderCell>, |
||||
].filter(Boolean), [sort, t]); |
||||
|
||||
const { value: data } = useEndpointData('email-inbox.list', query); |
||||
|
||||
const renderRow = useCallback(({ _id, name, email, active }) => <Table.Row action key={_id} onKeyDown={onClick(_id)} onClick={onClick(_id)} tabIndex={0} role='link'qa-room-id={_id}> |
||||
<Table.Cell withTruncatedText>{name}</Table.Cell> |
||||
<Table.Cell withTruncatedText>{email}</Table.Cell> |
||||
<Table.Cell withTruncatedText>{active ? t('Yes') : t('No')}</Table.Cell> |
||||
<SendTestButton id={_id} /> |
||||
</Table.Row>, [onClick, t]); |
||||
|
||||
return <GenericTable |
||||
header={header} |
||||
renderRow={renderRow} |
||||
results={data && data.emailInboxes} |
||||
total={data && data.total} |
||||
setParams={setParams} |
||||
params={params} |
||||
/>; |
||||
} |
||||
|
||||
export default EmailInboxTable; |
||||
@ -0,0 +1,11 @@ |
||||
import React from 'react'; |
||||
import { Box, Skeleton } from '@rocket.chat/fuselage'; |
||||
|
||||
export const FormSkeleton = (props) => <Box w='full' pb='x24' {...props}> |
||||
<Skeleton mbe='x8' /> |
||||
<Skeleton mbe='x4'/> |
||||
<Skeleton mbe='x4'/> |
||||
<Skeleton mbe='x8'/> |
||||
<Skeleton mbe='x4'/> |
||||
<Skeleton mbe='x8'/> |
||||
</Box>; |
||||
@ -0,0 +1,29 @@ |
||||
export interface IEmailInbox { |
||||
_id: string; |
||||
active: boolean; |
||||
name: string; |
||||
email: string; |
||||
description?: string; |
||||
senderInfo?: string; |
||||
department?: string; |
||||
smtp: { |
||||
server: string; |
||||
port: number; |
||||
username: string; |
||||
password: string; |
||||
secure: boolean; |
||||
}; |
||||
imap: { |
||||
server: string; |
||||
port: number; |
||||
username: string; |
||||
password: string; |
||||
secure: boolean; |
||||
}; |
||||
_createdAt: Date; |
||||
_createdBy: { |
||||
_id: string; |
||||
username: string; |
||||
}; |
||||
_updatedAt: Date; |
||||
} |
||||
@ -0,0 +1,146 @@ |
||||
import { EventEmitter } from 'events'; |
||||
|
||||
import IMAP from 'imap'; |
||||
import type Connection from 'imap'; |
||||
import { simpleParser, ParsedMail } from 'mailparser'; |
||||
|
||||
type IMAPOptions = { |
||||
deleteAfterRead: boolean; |
||||
filter: any[]; |
||||
rejectBeforeTS?: Date; |
||||
markSeen: boolean; |
||||
} |
||||
|
||||
export declare interface IMAPInterceptor { |
||||
on(event: 'email', listener: (email: ParsedMail) => void): this; |
||||
on(event: string, listener: Function): this; |
||||
} |
||||
|
||||
export class IMAPInterceptor extends EventEmitter { |
||||
private imap: IMAP; |
||||
|
||||
constructor( |
||||
imapConfig: IMAP.Config, |
||||
private options: IMAPOptions = { |
||||
deleteAfterRead: false, |
||||
filter: ['UNSEEN'], |
||||
markSeen: true, |
||||
}, |
||||
) { |
||||
super(); |
||||
|
||||
this.imap = new IMAP({ |
||||
connTimeout: 30000, |
||||
keepalive: true, |
||||
...imapConfig, |
||||
}); |
||||
|
||||
// On successfully connected.
|
||||
this.imap.on('ready', () => { |
||||
if (this.imap.state !== 'disconnected') { |
||||
this.openInbox((err) => { |
||||
if (err) { |
||||
throw err; |
||||
} |
||||
// fetch new emails & wait [IDLE]
|
||||
this.getEmails(); |
||||
|
||||
// If new message arrived, fetch them
|
||||
this.imap.on('mail', () => { |
||||
this.getEmails(); |
||||
}); |
||||
}); |
||||
} else { |
||||
this.log('IMAP did not connected.'); |
||||
this.imap.end(); |
||||
} |
||||
}); |
||||
|
||||
this.imap.on('error', (err: Error) => { |
||||
this.log('Error occurred ...'); |
||||
throw err; |
||||
}); |
||||
} |
||||
|
||||
log(...msg: any[]): void { |
||||
console.log(...msg); |
||||
} |
||||
|
||||
openInbox(cb: (error: Error, mailbox: Connection.Box) => void): void { |
||||
this.imap.openBox('INBOX', false, cb); |
||||
} |
||||
|
||||
start(): void { |
||||
this.imap.connect(); |
||||
} |
||||
|
||||
isActive(): boolean { |
||||
if (this.imap && this.imap.state && this.imap.state === 'disconnected') { |
||||
return false; |
||||
} |
||||
|
||||
return true; |
||||
} |
||||
|
||||
stop(callback = new Function()): void { |
||||
this.imap.end(); |
||||
this.imap.once('end', callback); |
||||
} |
||||
|
||||
restart(): void { |
||||
this.stop(() => { |
||||
this.log('Restarting IMAP ....'); |
||||
this.start(); |
||||
}); |
||||
} |
||||
|
||||
// Fetch all UNSEEN messages and pass them for further processing
|
||||
getEmails(): void { |
||||
this.imap.search(this.options.filter, (err, newEmails) => { |
||||
if (err) { |
||||
this.log(err); |
||||
throw err; |
||||
} |
||||
|
||||
// newEmails => array containing serials of unseen messages
|
||||
if (newEmails.length > 0) { |
||||
const fetch = this.imap.fetch(newEmails, { |
||||
bodies: ['HEADER', 'TEXT', ''], |
||||
struct: true, |
||||
markSeen: this.options.markSeen, |
||||
}); |
||||
|
||||
fetch.on('message', (msg, seqno) => { |
||||
msg.on('body', (stream, type) => { |
||||
if (type.which !== '') { |
||||
return; |
||||
} |
||||
|
||||
simpleParser(stream, (_err, email) => { |
||||
if (this.options.rejectBeforeTS && email.date && email.date < this.options.rejectBeforeTS) { |
||||
this.log('Rejecting email', email.subject); |
||||
return; |
||||
} |
||||
|
||||
this.emit('email', email); |
||||
}); |
||||
}); |
||||
|
||||
// On fetched each message, pass it further
|
||||
msg.once('end', () => { |
||||
// delete message from inbox
|
||||
if (this.options.deleteAfterRead) { |
||||
this.imap.seq.addFlags(seqno, 'Deleted', (err) => { |
||||
if (err) { this.log(`Mark deleted error: ${ err }`); } |
||||
}); |
||||
} |
||||
}); |
||||
}); |
||||
|
||||
fetch.once('error', (err) => { |
||||
this.log(`Fetch error: ${ err }`); |
||||
}); |
||||
} |
||||
}); |
||||
} |
||||
} |
||||
@ -0,0 +1,69 @@ |
||||
import { Meteor } from 'meteor/meteor'; |
||||
import nodemailer from 'nodemailer'; |
||||
import Mail from 'nodemailer/lib/mailer'; |
||||
|
||||
import { EmailInbox } from '../../../app/models/server/raw'; |
||||
import { IMAPInterceptor } from '../../email/IMAPInterceptor'; |
||||
import { IEmailInbox } from '../../../definition/IEmailInbox'; |
||||
import { onEmailReceived } from './EmailInbox_Incoming'; |
||||
|
||||
export type Inbox = { |
||||
imap: IMAPInterceptor; |
||||
smtp: Mail; |
||||
config: IEmailInbox; |
||||
} |
||||
|
||||
export const inboxes = new Map<string, Inbox>(); |
||||
|
||||
export async function configureEmailInboxes(): Promise<void> { |
||||
const emailInboxesCursor = EmailInbox.find({ |
||||
active: true, |
||||
}); |
||||
|
||||
for (const { imap } of inboxes.values()) { |
||||
imap.stop(); |
||||
} |
||||
|
||||
inboxes.clear(); |
||||
|
||||
for await (const emailInboxRecord of emailInboxesCursor) { |
||||
console.log('Setting up email interceptor for', emailInboxRecord.email); |
||||
|
||||
const imap = new IMAPInterceptor({ |
||||
password: emailInboxRecord.imap.password, |
||||
user: emailInboxRecord.imap.username, |
||||
host: emailInboxRecord.imap.server, |
||||
port: emailInboxRecord.imap.port, |
||||
tls: emailInboxRecord.imap.secure, |
||||
tlsOptions: { |
||||
rejectUnauthorized: false, |
||||
}, |
||||
// debug: (...args: any[]): void => console.log(...args),
|
||||
}, { |
||||
deleteAfterRead: false, |
||||
filter: [['UNSEEN'], ['SINCE', emailInboxRecord._updatedAt]], |
||||
rejectBeforeTS: emailInboxRecord._updatedAt, |
||||
markSeen: true, |
||||
}); |
||||
|
||||
imap.on('email', Meteor.bindEnvironment((email) => onEmailReceived(email, emailInboxRecord.email, emailInboxRecord.department))); |
||||
|
||||
imap.start(); |
||||
|
||||
const smtp = nodemailer.createTransport({ |
||||
host: emailInboxRecord.smtp.server, |
||||
port: emailInboxRecord.smtp.port, |
||||
secure: emailInboxRecord.smtp.secure, |
||||
auth: { |
||||
user: emailInboxRecord.smtp.username, |
||||
pass: emailInboxRecord.smtp.password, |
||||
}, |
||||
}); |
||||
|
||||
inboxes.set(emailInboxRecord.email, { imap, smtp, config: emailInboxRecord }); |
||||
} |
||||
} |
||||
|
||||
Meteor.startup(() => { |
||||
configureEmailInboxes(); |
||||
}); |
||||
@ -0,0 +1,203 @@ |
||||
/* eslint-disable @typescript-eslint/camelcase */ |
||||
import stripHtml from 'string-strip-html'; |
||||
import { Random } from 'meteor/random'; |
||||
import { ParsedMail, Attachment } from 'mailparser'; |
||||
import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; |
||||
|
||||
import { Livechat } from '../../../app/livechat/server/lib/Livechat'; |
||||
import { LivechatRooms, LivechatVisitors, Messages } from '../../../app/models/server'; |
||||
import { FileUpload } from '../../../app/file-upload/server'; |
||||
import { QueueManager } from '../../../app/livechat/server/lib/QueueManager'; |
||||
import { settings } from '../../../app/settings/server'; |
||||
|
||||
type FileAttachment = { |
||||
title: string; |
||||
title_link: string; |
||||
image_url?: string; |
||||
image_type?: string; |
||||
image_size?: string; |
||||
image_dimensions?: string; |
||||
audio_url?: string; |
||||
audio_type?: string; |
||||
audio_size?: string; |
||||
video_url?: string; |
||||
video_type?: string; |
||||
video_size?: string; |
||||
} |
||||
|
||||
const language = settings.get('Language') || 'en'; |
||||
const t = (s: string): string => TAPi18n.__(s, { lng: language }); |
||||
|
||||
function getGuestByEmail(email: string, name: string, department?: string): any { |
||||
const guest = LivechatVisitors.findOneGuestByEmailAddress(email); |
||||
|
||||
if (guest) { |
||||
return guest; |
||||
} |
||||
|
||||
const userId = Livechat.registerGuest({ |
||||
token: Random.id(), |
||||
name: name || email, |
||||
email, |
||||
department, |
||||
phone: undefined, |
||||
username: undefined, |
||||
connectionData: undefined, |
||||
}); |
||||
|
||||
const newGuest = LivechatVisitors.findOneById(userId, {}); |
||||
if (newGuest) { |
||||
return newGuest; |
||||
} |
||||
|
||||
throw new Error('Error getting guest'); |
||||
} |
||||
|
||||
async function uploadAttachment(attachment: Attachment, rid: string, visitorToken: string): Promise<FileAttachment> { |
||||
const details = { |
||||
name: attachment.filename, |
||||
size: attachment.size, |
||||
type: attachment.contentType, |
||||
rid, |
||||
visitorToken, |
||||
}; |
||||
|
||||
const fileStore = FileUpload.getStore('Uploads'); |
||||
return new Promise((resolve, reject) => { |
||||
fileStore.insert(details, attachment.content, function(err: any, file: any) { |
||||
if (err) { |
||||
reject(new Error(err)); |
||||
} |
||||
|
||||
const url = FileUpload.getPath(`${ file._id }/${ encodeURI(file.name) }`); |
||||
|
||||
const attachment: FileAttachment = { |
||||
title: file.name, |
||||
title_link: url, |
||||
}; |
||||
|
||||
if (/^image\/.+/.test(file.type)) { |
||||
attachment.image_url = url; |
||||
attachment.image_type = file.type; |
||||
attachment.image_size = file.size; |
||||
attachment.image_dimensions = file.identify != null ? file.identify.size : undefined; |
||||
} |
||||
|
||||
if (/^audio\/.+/.test(file.type)) { |
||||
attachment.audio_url = url; |
||||
attachment.audio_type = file.type; |
||||
attachment.audio_size = file.size; |
||||
} |
||||
|
||||
if (/^video\/.+/.test(file.type)) { |
||||
attachment.video_url = url; |
||||
attachment.video_type = file.type; |
||||
attachment.video_size = file.size; |
||||
} |
||||
|
||||
resolve(attachment); |
||||
}); |
||||
}); |
||||
} |
||||
|
||||
export async function onEmailReceived(email: ParsedMail, inbox: string, department?: string): Promise<void> { |
||||
if (!email.from?.value?.[0]?.address) { |
||||
return; |
||||
} |
||||
|
||||
const references = typeof email.references === 'string' ? [email.references] : email.references; |
||||
|
||||
const thread = references?.[0] ?? email.messageId; |
||||
|
||||
const guest = getGuestByEmail(email.from.value[0].address, email.from.value[0].name, department); |
||||
|
||||
let room = LivechatRooms.findOneByVisitorTokenAndEmailThread(guest.token, thread, {}); |
||||
if (room?.closedAt) { |
||||
room = await QueueManager.unarchiveRoom(room); |
||||
} |
||||
|
||||
let msg = email.text; |
||||
|
||||
if (email.html) { |
||||
// Try to remove the signature and history
|
||||
msg = stripHtml(email.html.replace(/<div name="messageSignatureSection.+/s, '')).result; |
||||
} |
||||
|
||||
const rid = room?._id ?? Random.id(); |
||||
const msgId = Random.id(); |
||||
|
||||
Livechat.sendMessage({ |
||||
guest, |
||||
message: { |
||||
_id: msgId, |
||||
groupable: false, |
||||
msg, |
||||
attachments: [ |
||||
{ |
||||
actions: [{ |
||||
type: 'button', |
||||
text: t('Reply_via_Email'), |
||||
msg: 'msg', |
||||
msgId, |
||||
msg_in_chat_window: true, |
||||
msg_processing_type: 'respondWithQuotedMessage', |
||||
}], |
||||
}, |
||||
], |
||||
blocks: [{ |
||||
type: 'context', |
||||
elements: [{ |
||||
type: 'mrkdwn', |
||||
text: `**${ t('From') }:** ${ email.from.text }\n**${ t('Subject') }:** ${ email.subject }`, |
||||
}], |
||||
}, { |
||||
type: 'section', |
||||
text: { |
||||
type: 'mrkdwn', |
||||
text: msg, |
||||
}, |
||||
}], |
||||
rid, |
||||
email: { |
||||
references, |
||||
messageId: email.messageId, |
||||
}, |
||||
}, |
||||
roomInfo: { |
||||
email: { |
||||
inbox, |
||||
thread, |
||||
replyTo: email.from.value[0].address, |
||||
subject: email.subject, |
||||
}, |
||||
}, |
||||
agent: undefined, |
||||
}).then(async () => { |
||||
if (!email.attachments.length) { |
||||
return; |
||||
} |
||||
|
||||
const attachments = []; |
||||
for await (const attachment of email.attachments) { |
||||
if (attachment.type !== 'attachment') { |
||||
continue; |
||||
} |
||||
|
||||
try { |
||||
attachments.push(await uploadAttachment(attachment, rid, guest.token)); |
||||
} catch (e) { |
||||
console.error('Error uploading attachment from email', e); |
||||
} |
||||
} |
||||
|
||||
Messages.update({ _id: msgId }, { |
||||
$addToSet: { |
||||
attachments: { |
||||
$each: attachments, |
||||
}, |
||||
}, |
||||
}); |
||||
}).catch((error) => { |
||||
console.log('Error receiving Email: %s', error.message); |
||||
}); |
||||
} |
||||
@ -0,0 +1,238 @@ |
||||
/* eslint-disable @typescript-eslint/camelcase */ |
||||
import Mail from 'nodemailer/lib/mailer'; |
||||
import { Match } from 'meteor/check'; |
||||
import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; |
||||
|
||||
import { callbacks } from '../../../app/callbacks/server'; |
||||
import { IEmailInbox } from '../../../definition/IEmailInbox'; |
||||
import { IUser } from '../../../definition/IUser'; |
||||
import { FileUpload } from '../../../app/file-upload/server'; |
||||
import { slashCommands } from '../../../app/utils/server'; |
||||
import { Messages, Rooms, Uploads, Users } from '../../../app/models/server'; |
||||
import { Inbox, inboxes } from './EmailInbox'; |
||||
import { sendMessage } from '../../../app/lib/server/functions/sendMessage'; |
||||
import { settings } from '../../../app/settings/server'; |
||||
|
||||
const livechatQuoteRegExp = /^\[\s\]\(https?:\/\/.+\/live\/.+\?msg=(?<id>.+?)\)\s(?<text>.+)/s; |
||||
|
||||
const user: IUser = Users.findOneById('rocket.cat'); |
||||
|
||||
const language = settings.get('Language') || 'en'; |
||||
const t = (s: string): string => TAPi18n.__(s, { lng: language }); |
||||
|
||||
const sendErrorReplyMessage = (error: string, options: any): void => { |
||||
if (!options?.rid || !options?.msgId) { |
||||
return; |
||||
} |
||||
|
||||
const message = { |
||||
groupable: false, |
||||
msg: `@${ options.sender } something went wrong when replying email, sorry. **Error:**: ${ error }`, |
||||
_id: String(Date.now()), |
||||
rid: options.rid, |
||||
ts: new Date(), |
||||
}; |
||||
|
||||
sendMessage(user, message, { _id: options.rid }); |
||||
}; |
||||
|
||||
function sendEmail(inbox: Inbox, mail: Mail.Options, options?: any): void { |
||||
inbox.smtp.sendMail({ |
||||
from: inbox.config.senderInfo ? { |
||||
name: inbox.config.senderInfo, |
||||
address: inbox.config.email, |
||||
} : inbox.config.email, |
||||
...mail, |
||||
}).then((info) => { |
||||
console.log('Message sent: %s', info.messageId); |
||||
}).catch((error) => { |
||||
console.log('Error sending Email reply: %s', error.message); |
||||
|
||||
if (!options?.msgId) { |
||||
return; |
||||
} |
||||
|
||||
sendErrorReplyMessage(error.message, options); |
||||
}); |
||||
} |
||||
|
||||
slashCommands.add('sendEmailAttachment', (command: any, params: string) => { |
||||
if (command !== 'sendEmailAttachment' || !Match.test(params, String)) { |
||||
return; |
||||
} |
||||
|
||||
const message = Messages.findOneById(params.trim()); |
||||
|
||||
if (!message || !message.file) { |
||||
return; |
||||
} |
||||
|
||||
const room = Rooms.findOneById(message.rid); |
||||
|
||||
const inbox = inboxes.get(room.email.inbox); |
||||
|
||||
if (!inbox) { |
||||
return sendErrorReplyMessage(`Email inbox ${ room.email.inbox } not found or disabled.`, { |
||||
msgId: message._id, |
||||
sender: message.u.username, |
||||
rid: room._id, |
||||
}); |
||||
} |
||||
|
||||
const file = Uploads.findOneById(message.file._id); |
||||
|
||||
FileUpload.getBuffer(file, (_err?: Error, buffer?: Buffer) => { |
||||
sendEmail(inbox, { |
||||
to: room.email.replyTo, |
||||
subject: room.email.subject, |
||||
text: message.attachments[0].description || '', |
||||
attachments: [{ |
||||
content: buffer, |
||||
contentType: file.type, |
||||
filename: file.name, |
||||
}], |
||||
inReplyTo: room.email.thread, |
||||
references: [ |
||||
room.email.thread, |
||||
], |
||||
}, |
||||
{ |
||||
msgId: message._id, |
||||
sender: message.u.username, |
||||
rid: message.rid, |
||||
}); |
||||
}); |
||||
|
||||
Messages.update({ _id: message._id }, { |
||||
$set: { |
||||
blocks: [{ |
||||
type: 'context', |
||||
elements: [{ |
||||
type: 'mrkdwn', |
||||
text: `**${ t('To') }:** ${ room.email.replyTo }\n**${ t('Subject') }:** ${ room.email.subject }`, |
||||
}], |
||||
}], |
||||
}, |
||||
$pull: { |
||||
attachments: { 'actions.0.type': 'button' }, |
||||
}, |
||||
}); |
||||
}, { |
||||
description: 'Send attachment as email', |
||||
params: 'msg_id', |
||||
}); |
||||
|
||||
callbacks.add('beforeSaveMessage', function(message: any, room: any) { |
||||
if (!room?.email?.inbox) { |
||||
return message; |
||||
} |
||||
|
||||
if (message.file) { |
||||
message.attachments.push({ |
||||
actions: [{ |
||||
type: 'button', |
||||
text: t('Send_via_Email_as_attachment'), |
||||
msg: `/sendEmailAttachment ${ message._id }`, |
||||
msg_in_chat_window: true, |
||||
msg_processing_type: 'sendMessage', |
||||
}], |
||||
}); |
||||
|
||||
return message; |
||||
} |
||||
|
||||
const { msg } = message; |
||||
|
||||
// Try to identify a quote in a livechat room
|
||||
const match = msg.match(livechatQuoteRegExp); |
||||
if (!match) { |
||||
return message; |
||||
} |
||||
|
||||
const inbox = inboxes.get(room.email.inbox); |
||||
|
||||
if (!inbox) { |
||||
sendErrorReplyMessage(`Email inbox ${ room.email.inbox } not found or disabled.`, { |
||||
msgId: message._id, |
||||
sender: message.u.username, |
||||
rid: room._id, |
||||
}); |
||||
|
||||
return message; |
||||
} |
||||
|
||||
if (!inbox) { |
||||
return message; |
||||
} |
||||
|
||||
const replyToMessage = Messages.findOneById(match.groups.id); |
||||
|
||||
if (!replyToMessage?.email?.messageId) { |
||||
return message; |
||||
} |
||||
|
||||
sendEmail(inbox, { |
||||
text: match.groups.text, |
||||
inReplyTo: replyToMessage.email.messageId, |
||||
references: [ |
||||
...replyToMessage.email.references ?? [], |
||||
replyToMessage.email.messageId, |
||||
], |
||||
to: room.email.replyTo, |
||||
subject: room.email.subject, |
||||
}, |
||||
{ |
||||
msgId: message._id, |
||||
sender: message.u.username, |
||||
rid: room._id, |
||||
}); |
||||
|
||||
message.msg = match.groups.text; |
||||
|
||||
message.groupable = false; |
||||
|
||||
message.blocks = [{ |
||||
type: 'context', |
||||
elements: [{ |
||||
type: 'mrkdwn', |
||||
text: `**${ t('To') }:** ${ room.email.replyTo }\n**${ t('Subject') }:** ${ room.email.subject }`, |
||||
}], |
||||
}, { |
||||
type: 'section', |
||||
text: { |
||||
type: 'mrkdwn', |
||||
text: message.msg, |
||||
}, |
||||
}, { |
||||
type: 'section', |
||||
text: { |
||||
type: 'mrkdwn', |
||||
text: `> ---\n${ replyToMessage.msg.replace(/^/gm, '> ') }`, |
||||
}, |
||||
}]; |
||||
|
||||
delete message.urls; |
||||
|
||||
return message; |
||||
}, callbacks.priority.LOW, 'ReplyEmail'); |
||||
|
||||
export async function sendTestEmailToInbox(emailInboxRecord: IEmailInbox, user: IUser): Promise<void> { |
||||
const inbox = inboxes.get(emailInboxRecord.email); |
||||
|
||||
if (!inbox) { |
||||
throw new Error('inbox-not-found'); |
||||
} |
||||
|
||||
const address = user.emails?.find((email) => email.verified)?.address; |
||||
|
||||
if (!address) { |
||||
throw new Error('user-without-verified-email'); |
||||
} |
||||
|
||||
console.log(`Sending testing email to ${ address }`); |
||||
sendEmail(inbox, { |
||||
to: address, |
||||
subject: 'Test of inbox configuration', |
||||
text: 'Test of inbox configuration successful', |
||||
}); |
||||
} |
||||
@ -0,0 +1,2 @@ |
||||
import './EmailInbox_Incoming'; |
||||
import './EmailInbox_Outgoing'; |
||||
Loading…
Reference in new issue