diff --git a/app/controllers/api/v1/inquiries.js b/app/controllers/api/v1/inquiries.js index 5bb6c2eef0..93c62400f2 100644 --- a/app/controllers/api/v1/inquiries.js +++ b/app/controllers/api/v1/inquiries.js @@ -6,6 +6,7 @@ const Boom = require('@hapi/boom'); const isSANB = require('is-string-and-not-blank'); const { isEmail } = require('validator'); +const { simpleParser } = require('mailparser'); const config = require('#config'); const env = require('#config/env'); @@ -22,6 +23,7 @@ function findHeaderByName(name, headers) { return null; } +// eslint-disable-next-line complexity async function create(ctx) { const { body } = ctx.request; @@ -37,6 +39,14 @@ async function create(ctx) { Boom.forbidden(ctx.translateError('INVALID_INQUIRY_WEBHOOK_REQUEST')) ); + let parsed; + try { + parsed = await simpleParser(body.raw); + } catch (err) { + ctx.logger.error(err); + return ctx.throw(err); + } + const { headerLines, session, text } = body; if (!session) return ctx.throw( @@ -48,6 +58,34 @@ async function create(ctx) { Boom.badRequest(ctx.translateError('INVALID_INQUIRY_WEBHOOK_PAYLOAD')) ); + if ( + (parsed.headers.has('Auto-submitted') && + parsed.headers.get('Auto-submitted') !== 'no') || + (parsed.headers.has('Auto-Submitted') && + parsed.headers.get('Auto-Submitted') !== 'no') || + (parsed.headers.has('X-Auto-Response-Suppress') && + ['dr', 'autoreply', 'auto-reply', 'auto_reply', 'all'].includes( + parsed.headers.get('X-Auto-Response-Suppress').toLowerCase().trim() + )) || + parsed.headers.has('List-Id') || + parsed.headers.has('List-id') || + parsed.headers.has('List-Unsubscribe') || + parsed.headers.has('List-unsubscribe') || + parsed.headers.has('Feedback-ID') || + parsed.headers.has('Feedback-Id') || + parsed.headers.has('X-Autoreply') || + parsed.headers.has('X-Auto-Reply') || + parsed.headers.has('X-AutoReply') || + parsed.headers.has('X-Autorespond') || + parsed.headers.has('X-Auto-Respond') || + parsed.headers.has('X-AutoRespond') || + (parsed.headers.has('Precedence') && + ['bulk', 'autoreply', 'auto-reply', 'auto_reply', 'list'].includes( + parsed.headers.get('Precedence').toLowerCase().trim() + )) + ) + return; + const { recipient, sender } = session; if (!recipient.includes('support@')) diff --git a/app/controllers/web/admin/inquiries.js b/app/controllers/web/admin/inquiries.js index 7bb628e346..573463350e 100644 --- a/app/controllers/web/admin/inquiries.js +++ b/app/controllers/web/admin/inquiries.js @@ -5,7 +5,6 @@ const { Buffer } = require('node:buffer'); const path = require('node:path'); -const process = require('node:process'); const getStream = require('get-stream'); const _ = require('lodash'); const Boom = require('@hapi/boom'); @@ -123,7 +122,9 @@ async function list(ctx) { updated_at: 1, reference: 1, email: 1, - plan: 1 + plan: 1, + is_resolved: 1, + is_denylist: 1 } }, { $match: query }, @@ -162,17 +163,21 @@ async function list(ctx) { async function retrieve(ctx) { const emailTemplatePath = path.join( - process.cwd(), - 'app/views/admin/inquiries/custom-email-previews.pug' + config.views.root, + 'admin/inquiries/custom-email-previews.pug' ); ctx.state.result = await Inquiries.findById(ctx.params.id); + ctx.state.messages = await Promise.all( ctx.state.result.messages.map(async (message) => { - message.html = await previewEmail(message.raw, { - template: emailTemplatePath, - ...config.previewEmailOptions - }); + if (message.raw) { + message.html = await previewEmail(message.raw, { + template: emailTemplatePath, + ...config.previewEmailOptions + }); + } + return message; }) ); @@ -318,6 +323,7 @@ async function bulkReply(ctx) { // eslint-disable-next-line no-await-in-loop const raw = await transporter.sendMail(info?.originalMessage); + inquiry.messages.push({ raw: raw.message }); // eslint-disable-next-line no-await-in-loop await inquiry.save(); diff --git a/app/views/admin/inquiries/_table.pug b/app/views/admin/inquiries/_table.pug index 43b601a36c..c61ebcd33f 100644 --- a/app/views/admin/inquiries/_table.pug +++ b/app/views/admin/inquiries/_table.pug @@ -26,10 +26,11 @@ include ../../_pagination tr td.align-middle.text-center .form-group.form-check.form-check-inline.mb-0 - input#is-inquiry-selected.form-check-input( + input.form-check-input( type="checkbox", name="is_inquiry_selected", - value=inquiry.id + value=inquiry.id, + data-email=inquiry.email ) td.align-middle a( @@ -81,9 +82,12 @@ include ../../_pagination button.close(type="button", data-dismiss="modal", aria-label="Close") span(aria-hidden="true") × .modal-body + .form-group + h5= t("Replying to") + #modal-reply-to-email-list .text-center form.form-group - label(for="bulk-reply-message") + label(for="textarea-bulk-reply-message") h5= t("Message") = " " textarea#textarea-bulk-reply-message.form-control( diff --git a/app/views/admin/inquiries/custom-email-previews.pug b/app/views/admin/inquiries/custom-email-previews.pug index 66ce4dcd3d..fcddb506cb 100644 --- a/app/views/admin/inquiries/custom-email-previews.pug +++ b/app/views/admin/inquiries/custom-email-previews.pug @@ -14,14 +14,6 @@ html min-height: 200px; display: block; } - .iframe-container { - width: 100%; - overflow-x: auto; - } - .iframe-content { - width: 1200px; - } - .preview-email-tabs { display: flex; flex-wrap: wrap; @@ -125,8 +117,7 @@ html else code= "Unnamed file" .preview-email-tabs - .iframe-container - iframe#html.iframe-content( + iframe#html( sandbox="", referrerpolicy="no-referrer", seamless="seamless", diff --git a/app/views/admin/inquiries/index.pug b/app/views/admin/inquiries/index.pug index c0fe4ecd17..681fc34ea8 100644 --- a/app/views/admin/inquiries/index.pug +++ b/app/views/admin/inquiries/index.pug @@ -1,5 +1,13 @@ extends ../../layout +block append scripts + script( + defer, + src=manifest("js/inquiries.js"), + integrity=manifest("js/inquiries.js", "integrity"), + crossorigin="anonymous" + ) + block body .container-fluid.py-3 .row.mt-1 diff --git a/app/views/admin/inquiries/retrieve.pug b/app/views/admin/inquiries/retrieve.pug index 8540f9a0ba..61fa157dc2 100644 --- a/app/views/admin/inquiries/retrieve.pug +++ b/app/views/admin/inquiries/retrieve.pug @@ -5,18 +5,17 @@ block body .row.mt-1 .col include ../../_breadcrumbs - .threaded-messages - each message in messages - - const isSenderEmail = message?.sender_email?.includes("support@"); - iframe.bg-white.border-dark( - sandbox="allow-downloads allow-scripts", - referrerpolicy="no-referrer", - seamless="seamless", - srcdoc=`${message.html}`, - height="400px", - width="100%" - ) - hr + each message in messages + - const isSenderEmail = message?.sender_email?.includes("support@"); + iframe.bg-white.border-dark( + sandbox="allow-downloads allow-scripts", + referrerpolicy="no-referrer", + seamless="seamless", + srcdoc=`${message.html}`, + height="400px", + width="100%" + ) + hr form.confirm-prompt( action=ctx.path, method="POST", @@ -25,7 +24,7 @@ block body .card.border-themed.card-custom .card-body .form-group.floating-label - label.read-only-message.text-muted= result.message + label.read-only-message.text-muted= result.message || result.text .form-group.floating-label textarea#input-message.form-control( rows="8", diff --git a/assets/css/_custom.scss b/assets/css/_custom.scss index 53fcb65084..582ac2a678 100644 --- a/assets/css/_custom.scss +++ b/assets/css/_custom.scss @@ -359,3 +359,17 @@ a, .markdown-body a { 40% {transform: translateY(-20px);} 60% {transform: translateY(-10px);} } + +#modal-reply-to-email-list { + list-style-type: none; + padding: 0; +} + +#modal-reply-to-email-list li { + padding: 8px 12px; + border-bottom: 1px solid #ccc; +} + +#modal-reply-to-email-list li:last-child { + border-bottom: none; +} \ No newline at end of file diff --git a/assets/js/core.js b/assets/js/core.js index 2ff243dbd1..97f6bb7e17 100644 --- a/assets/js/core.js +++ b/assets/js/core.js @@ -842,60 +842,3 @@ window.addEventListener( 'resize', setViewportProperty(document.documentElement) ); - -function handleBulkReply() { - const checkboxes = $('#table-inquiries input[type="checkbox"]:checked'); - const ids = checkboxes - .map(function () { - return $(this).val(); - }) - .get(); - - if (ids.length === 0) { - Swal.fire(window._types.error, 'No inquiries selected.', 'error'); - return; - } - - if (ids.length === 1) { - const { origin, pathname } = window.location; - const redirectUrl = `${origin}${pathname}/${ids[0]}`; - window.location.href = redirectUrl; - return; - } - - $('#bulk-reply-modal').modal('show'); -} - -async function handleSubmitBulkReply() { - const checkboxes = $('#table-inquiries input[type="checkbox"]:checked'); - const ids = checkboxes - .map(function () { - return $(this).val(); - }) - .get(); - - const message = $('#textarea-bulk-reply-message').val(); - - try { - spinner.show(); - - const url = `${window.location.pathname}/bulk`; - const response = await sendRequest({ ids, message }, url); - - if (response.err) { - console.log('error in response', { response }); - throw response.err; - } - - spinner.hide(); - - location.reload(true); - } catch (err) { - console.error(err); - spinner.hide(); - Swal.fire(window._types.error, err.message, 'error'); - } -} - -$('#table-inquiries').on('click', '#bulk-reply-button', handleBulkReply); -$('#table-inquiries').on('click', '#submit-bulk-reply', handleSubmitBulkReply); diff --git a/assets/js/inquiries.js b/assets/js/inquiries.js new file mode 100644 index 0000000000..5ed021d58c --- /dev/null +++ b/assets/js/inquiries.js @@ -0,0 +1,83 @@ +/** + * Copyright (c) Forward Email LLC + * SPDX-License-Identifier: BUSL-1.1 + */ + +const $ = require('jquery'); +const Swal = require('sweetalert2/dist/sweetalert2.js'); +const { spinner: Spinner } = require('@ladjs/assets'); + +const sendRequest = require('./send-request'); + +const spinner = new Spinner($); + +function handleBulkReply() { + const checkboxes = $('#table-inquiries input[type="checkbox"]:checked'); + const inquiries = checkboxes + .map(function () { + return { id: $(this).val(), email: $(this).data('email') }; + }) + .get(); + + const emails = inquiries.map((inquiry) => inquiry.email); + const uniqueEmails = [...new Set(emails)]; + + if (inquiries.length === 0) { + Swal.fire(window._types.error, 'No inquiries selected.', 'error'); + return; + } + + if (inquiries.length === 1) { + const { origin, pathname } = window.location; + const redirectUrl = `${origin}${pathname}/${inquiries[0].id}`; + window.location.href = redirectUrl; + return; + } + + const $emailList = $('#modal-reply-to-email-list'); + $emailList.empty(); + + for (const email of uniqueEmails) { + const listItem = $('
  • ').text(email); + $emailList.append(listItem); + } + + $('#bulk-reply-modal').modal('show'); +} + +async function handleSubmitBulkReply() { + console.log('testing'); + const checkboxes = $('#table-inquiries input[type="checkbox"]:checked'); + const inquiries = checkboxes + .map(function () { + return { id: $(this).val(), email: $(this).data('email') }; + }) + .get(); + + const ids = inquiries.map((inquiry) => inquiry.id); + + const message = $('#textarea-bulk-reply-message').val(); + + try { + spinner.show(); + + const url = `${window.location.pathname}/bulk`; + const response = await sendRequest({ ids, message }, url); + + if (response.err) { + console.log('error in response', { response }); + throw response.err; + } + + spinner.hide(); + + location.reload(true); + } catch (err) { + console.error(err); + spinner.hide(); + Swal.fire(window._types.error, err.message, 'error'); + } +} + +$('#table-inquiries').on('click', '#bulk-reply-button', handleBulkReply); +$('#submit-bulk-reply').on('click', handleSubmitBulkReply); diff --git a/scripts/migrate-inquiries-without-messages.js b/scripts/migrate-inquiries-without-messages.js index 11cddf4ea0..a3c992e8c1 100644 --- a/scripts/migrate-inquiries-without-messages.js +++ b/scripts/migrate-inquiries-without-messages.js @@ -55,7 +55,7 @@ graceful.listen(); for (const inquiry of inquiriesWithoutMessages) { console.log(`Attempting to migrate inquiry with id: ${inquiry._id}`); - if (!inquiry.subject || !inquiry.message) { + if (!inquiry.subject || !inquiry.message || !inquiry.text) { console.log('No subject or message found, skipping.'); continue; } @@ -85,7 +85,10 @@ graceful.listen(); locals: { user: user.toObject(), domains, - inquiry: { subject: inquiry.subject, message: inquiry?.message }, + inquiry: { + subject: inquiry.subject, + message: inquiry?.message || inquiry?.text + }, subject: inquiry.subject } }); @@ -95,7 +98,7 @@ graceful.listen(); const messages = [ { raw: info.raw, - text: inquiry.message + text: inquiry.message || inquiry?.text } ];