diff --git a/.eslintignore b/.eslintignore index 2865a05e..806070df 100644 --- a/.eslintignore +++ b/.eslintignore @@ -5,7 +5,9 @@ dist/ *.min.* # Per packages overrides -packages/editor/ - +packages/editor/* +public/lang/* packages/documentation/.vuepress/dist/ packages/documentation/api/ + +!packages/editor/src/js/vue/* \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..e6151805 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,31 @@ +name: Build + +on: + push: + branches: [ develop ] + pull_request: + branches: [ develop ] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Check out repository code + uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: '14.16.0' + cache: 'yarn' + - run: yarn install + - run: yarn build + unit-tests: + runs-on: ubuntu-latest + steps: + - name: Check out repository code + uses: actions/checkout@v2 + - uses: actions/setup-node@v2 + with: + node-version: '14.16.0' + cache: 'yarn' + - run: yarn install + - run: yarn test-ci diff --git a/.gitignore b/.gitignore index 20b84a93..94b63c2c 100644 --- a/.gitignore +++ b/.gitignore @@ -130,7 +130,7 @@ dist # TernJS port file .tern-port - +.vscode/** # Stores VSCode versions used for testing VSCode extensions .vscode-test diff --git a/.prettierignore b/.prettierignore index 2865a05e..41dde862 100644 --- a/.prettierignore +++ b/.prettierignore @@ -9,3 +9,4 @@ packages/editor/ packages/documentation/.vuepress/dist/ packages/documentation/api/ +packages/ui/helpers/locales diff --git a/jest.config.js b/jest.config.js index 81cc1c40..d5248edf 100644 --- a/jest.config.js +++ b/jest.config.js @@ -114,7 +114,7 @@ module.exports = { // restoreMocks: false, // The root directory that Jest should scan for tests and modules within - // rootDir: undefined, + rootDir: './packages', // A list of paths to directories that Jest should use to search for files in // roots: [ diff --git a/package.json b/package.json index 90377a2f..645616c6 100644 --- a/package.json +++ b/package.json @@ -109,6 +109,7 @@ "stream-buffers": "3.0.2", "validator": "10.11.0", "vue-i18n": "8.15.3", + "vue-select": "3.13.0", "vuelidate": "0.7.4", "xml2json": "0.12.0" }, diff --git a/packages/editor/src/css/badesender-image-gallery.less b/packages/editor/src/css/badesender-image-gallery.less index d3f539a0..c10129dc 100644 --- a/packages/editor/src/css/badesender-image-gallery.less +++ b/packages/editor/src/css/badesender-image-gallery.less @@ -176,7 +176,9 @@ padding: @dialog-margin; display: flex; justify-content: center; - + .ui-tabs-nav { + background: var(--v-secondary-base) !important; + } .tabs_horizontal { background: white; width: 100%; @@ -185,6 +187,7 @@ } .close { visibility: hidden; + color: #fff; cursor: pointer; width: 24px; display: block; diff --git a/packages/editor/src/css/badsender-editor.less b/packages/editor/src/css/badsender-editor.less index f95aef2d..bda891a3 100644 --- a/packages/editor/src/css/badsender-editor.less +++ b/packages/editor/src/css/badsender-editor.less @@ -1,5 +1,4 @@ // this the equivalent of app_standalone_material.less - @import '../../../../node_modules/font-awesome/less/font-awesome.less'; @import 'style_mosaico.less'; @@ -8,6 +7,9 @@ @import (less) '../../../../node_modules/toastr/toastr.less'; +@import (less) '../../../../node_modules/vue-select/dist/vue-select.css'; +@import (less) 'custom-select-style.less'; + // Override variables @tab-text-color: #555; diff --git a/packages/editor/src/css/custom-select-style.less b/packages/editor/src/css/custom-select-style.less new file mode 100644 index 00000000..0595dde6 --- /dev/null +++ b/packages/editor/src/css/custom-select-style.less @@ -0,0 +1,13 @@ +.vs__dropdown-toggle, +.vs__dropdown-menu { + border: none !important; +} + +.vs__selected { + position: absolute; + top: 12px; +} + +.vs__actions > button { + padding-right: 10px; +} \ No newline at end of file diff --git a/packages/editor/src/js/ext/badsender-server-storage.js b/packages/editor/src/js/ext/badsender-server-storage.js index 1fb4a5c7..d61659b0 100644 --- a/packages/editor/src/js/ext/badsender-server-storage.js +++ b/packages/editor/src/js/ext/badsender-server-storage.js @@ -26,6 +26,7 @@ function loader(opts) { name: 'Save', // l10n happens in the template enabled: ko.observable(true), }; + saveCmd.execute = function () { saveCmd.enabled(false); var data = getData(viewModel); @@ -49,7 +50,6 @@ function loader(opts) { // use callback for easier jQuery updates // => Deprecation notice for .success(), .error(), and .complete() function onPostSuccess(data, textStatus, jqXHR) { - console.log('save success'); viewModel.notifier.success(viewModel.t('save-message-success')); } @@ -77,54 +77,7 @@ function loader(opts) { enabled: ko.observable(true), }; testCmd.execute = function () { - console.info('TEST'); - console.log(viewModel.metadata.url.send); - testCmd.enabled(false); - var email = viewModel.t('Insert here the recipient email address'); - email = global.prompt(viewModel.t('Test email address'), email); - - // Don't validate `null` values => isEmail will error - if (!email) return testCmd.enabled(true); - - const emails = email.split(";"); - for (const address of emails){ - if (!isEmail(address)) { - global.alert(viewModel.t('Invalid email address')); - return testCmd.enabled(true); - } - } - - console.log('TODO testing...', email); - var metadata = ko.toJS(viewModel.metadata); - var datas = { - rcpt: email, - html: viewModel.exportHTML(), - }; - $.ajax({ - url: viewModel.metadata.url.send, - method: 'POST', - data: datas, - success: onTestSuccess, - error: onTestError, - complete: onTestComplete, - }); - - function onTestSuccess(data, textStatus, jqXHR) { - console.log('test success'); - viewModel.notifier.success(viewModel.t('Test email sent...')); - } - - function onTestError(jqXHR, textStatus, errorThrown) { - console.log('test error'); - console.log(errorThrown); - viewModel.notifier.error( - viewModel.t('Unexpected error talking to server: contact us!') - ); - } - - function onTestComplete() { - testCmd.enabled(true); - } + viewModel.openTestModal(true); }; ////// diff --git a/packages/editor/src/js/viewmodel.js b/packages/editor/src/js/viewmodel.js index ebcb391e..e8dc180e 100644 --- a/packages/editor/src/js/viewmodel.js +++ b/packages/editor/src/js/viewmodel.js @@ -43,6 +43,7 @@ function initializeEditor(content, blockDefs, thumbPathConverter, galleryUrl) { debug: ko.observable(false), contentListeners: ko.observable(0), loadedTemplate: ko.observable(false), + openTestModal: ko.observable(false), logoPath: 'rs/img/mosaico32.png', logoUrl: '.', logoAlt: 'mosaico', diff --git a/packages/editor/src/js/vue/components/esp/esp-send-mail.js b/packages/editor/src/js/vue/components/esp/esp-send-mail.js index 81f96d54..8312dc5f 100644 --- a/packages/editor/src/js/vue/components/esp/esp-send-mail.js +++ b/packages/editor/src/js/vue/components/esp/esp-send-mail.js @@ -1,78 +1,47 @@ -var Vue = require('vue/dist/vue.common'); -var { SendinBlueComponent } = require('./providers/SendinBlueComponent'); -var { ActitoComponent } = require('./providers/ActitoComponent'); -var { ProfileListComponent } = require('../esp/profile-list'); -var { getEspIds } = require('../../utils/apis'); -var { SEND_MODE } = require('../../constant/send-mode'); -var { ESP_TYPE } = require('../../constant/esp-type'); +const Vue = require('vue/dist/vue.common'); +const { SendinBlueComponent } = require('./providers/SendinBlueComponent'); +const { ActitoComponent } = require('./providers/ActitoComponent'); +const { ModalComponent } = require('../modal/modalComponent'); +const { getEspIds } = require('../../utils/apis'); +const { SEND_MODE } = require('../../constant/send-mode'); +const { ESP_TYPE } = require('../../constant/esp-type'); -var { getCampaignDetail, getProfileDetail } = require('../../../vue/utils/apis'); -var axios = require('axios'); +const { getCampaignDetail, getProfileDetail } = require('../../../vue/utils/apis'); +const axios = require('axios'); -const EspComponent = Vue.component('esp-form', { +const EspComponent = Vue.component('EspForm', { components: { SendinBlueComponent, ActitoComponent, - ProfileListComponent + ModalComponent }, props: { vm: { type: Object, default: () => ({}) }, }, - template: ` -
-
-
-
-
-
-
-
-
-
-
-
+
\ No newline at end of file
diff --git a/packages/server/emails-group/emails-group.controller.js b/packages/server/emails-group/emails-group.controller.js
new file mode 100644
index 00000000..76a508d0
--- /dev/null
+++ b/packages/server/emails-group/emails-group.controller.js
@@ -0,0 +1,143 @@
+'use-strict';
+
+const asyncHandler = require('express-async-handler');
+const logger = require('../utils/logger');
+
+const emailsGroupService = require('./emails-group.service');
+
+module.exports = {
+ listEmailsGroups: asyncHandler(listEmailGroups),
+ createEmailsGroup: asyncHandler(createEmailGroup),
+ getEmailsGroup: asyncHandler(getEmailsGroup),
+ deleteEmailsGroup: asyncHandler(deleteEmailsGroup),
+ editEmailsGroup: asyncHandler(editEmailGroup),
+};
+
+/**
+ * @api {get} /emails-group list of emails groups
+ * @apiPermission group_admin
+ * @apiName listEmailsGroups
+ * @apiGroup EmailsGroups
+ *
+ * @apiUse emailsGroup
+ * @apiSuccess {emailsGroup[]} items list of emails groups
+ */
+async function listEmailGroups(req, res) {
+ logger.log('emailsGroupController:listEmailsGroups');
+ const { user } = req;
+ const emailsGroups = await emailsGroupService.listEmailsGroups(
+ user?.group?.id
+ );
+ res.json({
+ items: emailsGroups,
+ });
+}
+
+/**
+ * @api {post} /emails-group/ create emails group
+ * @apiPermission group_admin
+ * @apiName CreateEmailsGroup
+ * @apiGroup emailsGroups
+ *
+ * @apiParam (Body) {String} name
+ * @apiParam (Body) {String} emails
+ *
+ * @apiUse emailsGroup
+ * @apiSuccess {emailsGroup} created Emails group
+ */
+async function createEmailGroup(req, res) {
+ logger.log('emailsGroupController:createEmailsGroup');
+ const {
+ user,
+ body: { name, emails },
+ } = req;
+
+ const emailGroup = await emailsGroupService.createEmailsGroup({
+ name,
+ emails,
+ user,
+ });
+
+ res.json({
+ emailGroup,
+ });
+}
+
+/**
+ * @api {get} /email-group/:emailsGroupId Emails Group update
+ * @apiPermission group_admin
+ * @apiName getEmailsGroup
+ * @apiGroup EmailsGroup
+ *
+ * @apiUse emailsGroup
+ * @apiSuccess {EmailsGroup} get Emails group from id
+ */
+async function getEmailsGroup(req, res) {
+ logger.log('emailsGroupController:getEmailsGroup');
+ const {
+ user,
+ params: { emailsGroupId },
+ } = req;
+
+ const emailsGroup = await emailsGroupService.getEmailsGroup({
+ emailsGroupId,
+ userGroupId: user?.group?.id,
+ });
+
+ res.send(emailsGroup);
+}
+
+/**
+ * @api {delete} /emails-group/:emailsGroupId emails group delete
+ * @apiPermission group_admin
+ * @apiName DeleteEmailsGroup
+ * @apiGroup EmailsGroups
+ *
+ * @apiUse emailsGroup
+ * @apiSuccess {emailsGroup} emailsGroup deleted
+ */
+
+async function deleteEmailsGroup(req, res) {
+ logger.log('emailsGroupController:deleteEmailsGroup');
+ const {
+ user,
+ params: { emailsGroupId },
+ } = req;
+
+ await emailsGroupService.deleteEmailsGroup({ user, emailsGroupId });
+
+ res.status(204).send();
+}
+
+/**
+ * @api {put} /emails-group/:emailsGroupId email group edit
+ * @apiPermission group_admin
+ * @apiName PatchEmailsGroup
+ * @apiGroup EmailsGroup
+ *
+ * @apiParam {string} emailsGroupId
+ *
+ * @apiParam (Body) {String} name
+ * @apiParam (Body) {String} emails
+ *
+ * @apiUse emailsGroup
+ * @apiSuccess {emailsGroup} emails group edited
+ */
+
+async function editEmailGroup(req, res) {
+ logger.log('emailGroupController:editEmailGroup');
+ const {
+ user,
+ params: { emailsGroupId },
+ body: { name, emails },
+ } = req;
+
+ await emailsGroupService.editEmailsGroup({
+ emailsGroupId,
+ name,
+ emails,
+ user,
+ });
+
+ res.status(204).send();
+}
diff --git a/packages/server/emails-group/emails-group.routes.js b/packages/server/emails-group/emails-group.routes.js
new file mode 100644
index 00000000..99785980
--- /dev/null
+++ b/packages/server/emails-group/emails-group.routes.js
@@ -0,0 +1,19 @@
+'use strict';
+
+const express = require('express');
+const router = express.Router();
+
+const { GUARD_GROUP_ADMIN, GUARD_USER } = require('../account/auth.guard');
+const emailsGroup = require('./emails-group.controller');
+
+router.get('', GUARD_USER, emailsGroup.listEmailsGroups);
+router.post('', GUARD_GROUP_ADMIN, emailsGroup.createEmailsGroup);
+router.get('/:emailsGroupId', GUARD_USER, emailsGroup.getEmailsGroup);
+router.delete(
+ '/:emailsGroupId',
+ GUARD_GROUP_ADMIN,
+ emailsGroup.deleteEmailsGroup
+);
+router.patch('/:emailsGroupId', GUARD_GROUP_ADMIN, emailsGroup.editEmailsGroup);
+
+module.exports = router;
diff --git a/packages/server/emails-group/emails-group.schema.js b/packages/server/emails-group/emails-group.schema.js
new file mode 100644
index 00000000..b2dfcba6
--- /dev/null
+++ b/packages/server/emails-group/emails-group.schema.js
@@ -0,0 +1,38 @@
+'use strict';
+
+const { Schema } = require('mongoose');
+const { ObjectId } = Schema.Types;
+const { GroupModel } = require('../constant/model.names.js');
+
+/**
+ * @apiDefine emailsGroup
+ * @apiSuccess {String} id
+ * @apiSuccess {String} name
+ * @apiSuccess {String} emails
+ * @apiSuccess {String} _company
+ */
+
+const EmailsGroupSchema = Schema(
+ {
+ name: {
+ type: String,
+ required: [true, 'Emails group name is required'],
+ },
+ emails: {
+ type: String,
+ required: [true, 'Emails group emails list is required'],
+ },
+ _company: {
+ type: ObjectId,
+ ref: GroupModel,
+ alias: 'group',
+ },
+ },
+ {
+ timestamps: true,
+ toJSON: { virtuals: true },
+ toObject: { virtuals: true },
+ }
+);
+
+module.exports = EmailsGroupSchema;
diff --git a/packages/server/emails-group/emails-group.service.js b/packages/server/emails-group/emails-group.service.js
new file mode 100644
index 00000000..31534730
--- /dev/null
+++ b/packages/server/emails-group/emails-group.service.js
@@ -0,0 +1,133 @@
+'use strict';
+
+const { Types } = require('mongoose');
+
+const { EmailsGroups } = require('../common/models.common.js');
+const {
+ NotFound,
+ InternalServerError,
+ Conflict,
+ BadRequest,
+ Unauthorized,
+} = require('http-errors');
+const ERROR_CODES = require('../constant/error-codes.js');
+const logger = require('../utils/logger.js');
+
+module.exports = {
+ listEmailsGroups,
+ createEmailsGroup,
+ getEmailsGroup,
+ deleteEmailsGroup,
+ editEmailsGroup,
+};
+
+// TODO complete listEmailGroups
+async function listEmailsGroups(groupId) {
+ logger.log('emailsGroupService:ListEmailsGroups');
+ if (!groupId) {
+ throw new BadRequest(ERROR_CODES.MISSING_GROUP_PARAM);
+ }
+
+ return EmailsGroups.find({
+ _company: groupId,
+ }).sort({ name: 1 });
+}
+
+async function createEmailsGroup({ name, emails, user }) {
+ logger.log('emailsGroupService:createEmailsGroup');
+ checkNameAndEmailsExists({ name, emails });
+
+ if (!user) {
+ throw new Unauthorized(ERROR_CODES.UNAUTHORIZED);
+ }
+
+ if (await EmailsGroups.exists({ name, _company: user.group.id })) {
+ throw new Conflict(ERROR_CODES.EMAIL_GROUP_NAME_ALREADY_EXIST);
+ }
+
+ return EmailsGroups.create({
+ name,
+ emails,
+ _company: user.group.id,
+ });
+}
+
+async function getEmailsGroup({ emailsGroupId, userGroupId }) {
+ logger.log('emailsGroupService:getEmailsGroup');
+ await checkIfEmailGroupExist(emailsGroupId);
+
+ const emailsGroup = await EmailsGroups.findById(emailsGroupId);
+
+ if (emailsGroup.group?.toString() !== userGroupId) {
+ throw new NotFound(ERROR_CODES.EMAIL_GROUP_NOT_FOUND);
+ }
+
+ return emailsGroup;
+}
+
+async function editEmailsGroup({ emailsGroupId, name, emails, user }) {
+ logger.log('emailsGroupService:editEmailsGroup');
+ checkNameAndEmailsExists({ name, emails });
+
+ if (!user) {
+ throw new Unauthorized(ERROR_CODES.UNAUTHORIZED);
+ }
+
+ await checkIfEmailGroupExist(emailsGroupId);
+
+ const emailsGroup = await EmailsGroups.findById(emailsGroupId);
+
+ if (emailsGroup.group?.toString() !== user.group.id) {
+ throw new NotFound(ERROR_CODES.EMAIL_GROUP_NOT_FOUND);
+ }
+
+ return EmailsGroups.updateOne(
+ { _id: Types.ObjectId(emailsGroupId) },
+ {
+ name,
+ emails,
+ }
+ );
+}
+
+async function deleteEmailsGroup({ emailsGroupId, user }) {
+ logger.log('emailsGroupService:deleteEmailsGroup');
+ const emailsGroup = await findOne(emailsGroupId);
+
+ const deleteEmailsGroupResponse = await deleteOne(emailsGroup.id);
+
+ if (emailsGroup.group?.toString() !== user.group.id) {
+ throw new NotFound(ERROR_CODES.EMAIL_GROUP_NOT_FOUND);
+ }
+
+ if (deleteEmailsGroupResponse.ok !== 1) {
+ throw new InternalServerError(ERROR_CODES.FAILED_EMAIL_GROUP_DELETE);
+ }
+
+ return deleteEmailsGroupResponse;
+}
+
+async function deleteOne(emailsGroupId) {
+ return EmailsGroups.deleteOne({ _id: Types.ObjectId(emailsGroupId) });
+}
+
+async function findOne(emailsGroupId) {
+ await checkIfEmailGroupExist(emailsGroupId);
+ return EmailsGroups.findOne({ _id: Types.ObjectId(emailsGroupId) });
+}
+
+async function checkIfEmailGroupExist(emailsGroupId) {
+ if (!(await EmailsGroups.exists({ _id: Types.ObjectId(emailsGroupId) }))) {
+ throw new NotFound(ERROR_CODES.EMAIL_GROUP_NOT_FOUND);
+ }
+}
+
+function checkNameAndEmailsExists({ name, emails }) {
+ if (!name) {
+ throw new BadRequest(ERROR_CODES.MISSING_EMAIL_GROUP_NAME_PARAM);
+ }
+
+ if (!emails) {
+ throw new BadRequest(ERROR_CODES.MISSING_EMAIL_GROUP_NAME_PARAM);
+ }
+}
diff --git a/packages/server/esp/actito/actitoProvider.js b/packages/server/esp/actito/actitoProvider.js
index 06fc6420..d2b96587 100644
--- a/packages/server/esp/actito/actitoProvider.js
+++ b/packages/server/esp/actito/actitoProvider.js
@@ -110,7 +110,7 @@ class ActitoProvider {
});
const {
- name,
+ displayName,
from,
replyTo,
entityOfTarget,
@@ -120,7 +120,7 @@ class ActitoProvider {
} = apiEmailCampaignResult?.data;
return {
- name: name,
+ name: displayName,
additionalApiData: {
senderMail: from,
replyTo: replyTo,
@@ -292,6 +292,7 @@ class ActitoProvider {
headers: headerAccess,
}
),
+ isEdit: true,
});
}
@@ -302,6 +303,7 @@ class ActitoProvider {
mailingId,
entity,
mailCampaignApi,
+ isEdit = false,
}) {
let campaignId;
@@ -314,7 +316,7 @@ class ActitoProvider {
from,
entityOfTarget,
supportedLanguages,
- name,
+ displayName,
replyTo,
targetTable,
encoding,
@@ -329,16 +331,27 @@ class ActitoProvider {
mailingId,
});
- const createdCampaignMailResult = await mailCampaignApi({
+ let mailCampaignApiData = {
from,
entityOfTarget,
supportedLanguages,
- name,
+ displayName,
replyTo,
targetTable,
encoding,
contentType,
- });
+ };
+
+ if (!isEdit) {
+ mailCampaignApiData = {
+ ...mailCampaignApiData,
+ name: displayName?.replace(/[^A-Z0-9]+/gi, '_'),
+ };
+ }
+
+ const createdCampaignMailResult = await mailCampaignApi(
+ mailCampaignApiData
+ );
if (!createdCampaignMailResult?.data?.campaignId) {
throw new InternalServerError(ERROR_CODES.UNEXPECTED_ESP_RESPONSE);
@@ -370,19 +383,19 @@ class ActitoProvider {
from,
entityOfTarget,
supportedLanguages,
- name,
+ displayName,
replyTo,
targetTable,
encoding,
subject,
};
} catch (e) {
- logger.error(e.response.data);
+ logger.error(e?.response?.data);
if (campaignId) {
await this.deleteCampaignMail({ campaignId, entity });
}
- if (e.response.status === 409) {
+ if (e?.response?.status === 409) {
throw new Conflict(ERROR_CODES.ALREADY_USED_MAIL_NAME);
}
@@ -412,7 +425,7 @@ class ActitoProvider {
senderMail: from,
entity: entityOfTarget,
supportedLanguage,
- name,
+ name: displayName,
replyTo,
targetTable,
encodingType: encoding,
@@ -429,7 +442,7 @@ class ActitoProvider {
from,
entityOfTarget,
supportedLanguages: [supportedLanguage],
- name,
+ displayName,
replyTo,
targetTable,
encoding,
diff --git a/packages/server/group/group.controller.js b/packages/server/group/group.controller.js
index c2b17ef7..c6333b31 100644
--- a/packages/server/group/group.controller.js
+++ b/packages/server/group/group.controller.js
@@ -1,7 +1,7 @@
'use strict';
const { pick } = require('lodash');
-const createError = require('http-errors');
+const { NotFound } = require('http-errors');
const asyncHandler = require('express-async-handler');
const {
createWorkspace,
@@ -10,6 +10,7 @@ const {
const groupService = require('../group/group.service.js');
const profileService = require('../profile/profile.service.js');
+const emailsGroupService = require('../emails-group/emails-group.service.js');
const { Groups, Templates, Mailings } = require('../common/models.common.js');
@@ -23,6 +24,7 @@ module.exports = {
readMailings: asyncHandler(readMailings),
readProfiles: asyncHandler(readProfiles),
readWorkspaces: asyncHandler(readWorkspaces),
+ readEmailGroups: asyncHandler(readEmailGroups),
update: asyncHandler(update),
};
@@ -103,7 +105,7 @@ async function seedGroups(req, res) {
async function read(req, res) {
const { groupId } = req.params;
const group = await Groups.findById(groupId);
- if (!group) throw new createError.NotFound();
+ if (!group) throw new NotFound();
res.json(group);
}
@@ -146,10 +148,29 @@ async function readTemplates(req, res) {
Groups.findById(groupId).select('_id'),
Templates.findForApi({ _company: groupId }),
]);
- if (!group) throw new createError.NotFound();
+ if (!group) throw new NotFound();
res.json({ items: templates });
}
+/**
+ * @api {get} /groups/:groupId/email-groups group emails
+ * @apiPermission group_admin
+ * @apiName GetEmailGroups
+ * @apiGroup Groups
+ *
+ * @apiParam {string} groupId
+ *
+ * @apiUse emailsGroup
+ * @apiSuccess {emailGroup[]} items list of email groups
+ */
+
+async function readEmailGroups(req, res) {
+ const { groupId } = req.params;
+ const emailGroups = await emailsGroupService.listEmailsGroups(groupId);
+ if (!emailGroups) throw new NotFound();
+ res.json({ items: emailGroups });
+}
+
/**
* @api {GET} /groups/:groupId/profiles group profiles
* @apiPermission admin
@@ -186,7 +207,7 @@ async function readMailings(req, res) {
Groups.findById(groupId).select('_id'),
Mailings.findForApi({ _company: groupId }),
]);
- if (!group) throw new createError.NotFound();
+ if (!group) throw new NotFound();
res.json({ items: mailings });
}
@@ -204,7 +225,7 @@ async function readMailings(req, res) {
async function readWorkspaces(req, res, next) {
const { groupId } = req.params;
- if (!groupId) next(new createError.NotFound());
+ if (!groupId) next(new NotFound());
const workspaces = await findWorkspaces({ groupId });
return res.json({ items: workspaces });
}
diff --git a/packages/server/group/group.routes.js b/packages/server/group/group.routes.js
index 33c8360a..d422fe0e 100644
--- a/packages/server/group/group.routes.js
+++ b/packages/server/group/group.routes.js
@@ -9,6 +9,7 @@ const {
GUARD_ADMIN,
GUARD_GROUP_ADMIN,
guard,
+ GUARD_USER,
} = require('../account/auth.guard.js');
const Roles = require('../account/roles.js');
const groups = require('./group.controller.js');
@@ -50,6 +51,12 @@ router.get(
groups.readWorkspaces
);
+router.get(
+ '/:groupId/email-groups',
+ GUARD_USER, // guard() will check if the user is logged
+ groups.readEmailGroups
+);
+
router.get(
'/:groupId/mailings',
guard(), // guard() will check if the user is logged
diff --git a/packages/server/html-templates/mosaico-editor.pug b/packages/server/html-templates/mosaico-editor.pug
index bd57cbdb..c2e17309 100644
--- a/packages/server/html-templates/mosaico-editor.pug
+++ b/packages/server/html-templates/mosaico-editor.pug
@@ -12,7 +12,7 @@ html(lang=getLocale())
link(rel="canonical" href="http://agence.badsender.com")
link(rel="shortcut icon" href="/favicon.png" type="image/png")
link(rel="icon" href="/favicon.png" type="image/png")
-
+
link(href='https://fonts.googleapis.com/css?family=PT+Sans:400,700' rel='stylesheet' type='text/css')
meta(name="viewport" content="width=1024, initial-scale=1")
diff --git a/packages/server/index.js b/packages/server/index.js
index 285a8b17..f17d28f2 100644
--- a/packages/server/index.js
+++ b/packages/server/index.js
@@ -30,6 +30,7 @@ const templateRouter = require('./template/template.routes');
const userRouter = require('./user/user.routes');
const imageRouter = require('./image/image.routes');
const accountRouter = require('./account/account.routes');
+const EmailGroupRouter = require('./emails-group/emails-group.routes');
const workers =
process.env.WORKERS <= require('os').cpus().length ? process.env.WORKERS : 1;
@@ -252,6 +253,7 @@ if (cluster.isMaster) {
app.use('/api/templates', templateRouter);
app.use('/api/users', userRouter);
app.use('/api/images', imageRouter);
+ app.use('/api/emails-groups', EmailGroupRouter);
app.use('/api/account', accountRouter);
app.use('/api/version', versionRouter);
diff --git a/packages/server/mailing/download-zip.controller.js b/packages/server/mailing/download-zip.controller.js
index c4f85c50..fc5d46e0 100644
--- a/packages/server/mailing/download-zip.controller.js
+++ b/packages/server/mailing/download-zip.controller.js
@@ -3,9 +3,15 @@
// const cheerio = require('cheerio')
const asyncHandler = require('express-async-handler');
const mailingService = require('./mailing.service.js');
+const logger = require('../utils/logger.js');
const archiver = require('archiver');
+const { InternalServerError } = require('http-errors');
+const { ERROR_CODES } = require('../constant/error-codes.js');
-module.exports = asyncHandler(downloadZip);
+module.exports = {
+ downloadZip: asyncHandler(downloadZip),
+ downloadMultipleZip: asyncHandler(downloadMultipleZip),
+};
// eslint-disable-next-line no-unused-vars
function isHttpUrl(uri) {
@@ -30,6 +36,8 @@ function isHttpUrl(uri) {
// https://github.com/archiverjs/node-archiver/blob/master/examples/express.js
// we need to keep the `next` callback to handle zip events
async function downloadZip(req, res, next) {
+ logger.log('Calling downloadZip');
+
const { user, body } = req;
const { mailingId } = req.params;
const { html, ...downloadOptions } = body;
@@ -58,3 +66,35 @@ async function downloadZip(req, res, next) {
// set the archive name
processedArchive.finalize();
}
+
+async function downloadMultipleZip(req, res, next) {
+ logger.log('Calling downloadMultipleZip');
+ const { user, body } = req;
+ const { mailingIds, downloadOptions } = body;
+ const archive = archiver('zip');
+ archive.on('error', next);
+
+ if (!downloadOptions) {
+ throw new InternalServerError(ERROR_CODES.MISSING_DOWNLOAD_OPTIONS);
+ }
+
+ const {
+ archive: processedArchive,
+ name,
+ } = await mailingService.downloadMultipleZip({
+ user,
+ archive,
+ mailingIds,
+ downloadOptions,
+ });
+ res.header('Content-Type', 'application/zip');
+ res.header('Content-Disposition', `attachment; filename="${name}.zip"`);
+
+ archive.on('end', () => {
+ console.log(`Archive wrote ${archive.pointer()} bytes`);
+ res.end();
+ });
+ archive.pipe(res);
+
+ processedArchive.finalize();
+}
diff --git a/packages/server/mailing/mailing.controller.js b/packages/server/mailing/mailing.controller.js
index 9100778b..aa3c981b 100644
--- a/packages/server/mailing/mailing.controller.js
+++ b/packages/server/mailing/mailing.controller.js
@@ -14,7 +14,10 @@ const simpleI18n = require('../helpers/server-simple-i18n.js');
const logger = require('../utils/logger.js');
const { Mailings, Galleries, Users } = require('../common/models.common.js');
const sendTestMail = require('./send-test-mail.controller.js');
-const downloadZip = require('./download-zip.controller.js');
+const {
+ downloadZip,
+ downloadMultipleZip,
+} = require('./download-zip.controller.js');
const cleanTagName = require('../helpers/clean-tag-name.js');
const fileManager = require('../common/file-manage.service.js');
const modelsUtils = require('../utils/model.js');
@@ -36,6 +39,7 @@ module.exports = {
duplicate: asyncHandler(duplicate),
updateMosaico: asyncHandler(updateMosaico),
bulkUpdate: asyncHandler(bulkUpdate),
+ downloadMultipleZip: asyncHandler(downloadMultipleZip),
bulkDestroy: asyncHandler(bulkDestroy),
delete: asyncHandler(deleteMailing),
transferToUser: asyncHandler(transferToUser),
@@ -152,8 +156,10 @@ async function read(req, res) {
async function readMosaico(req, res) {
const { mailingId } = req.params;
+ const { user } = req;
const query = modelsUtils.addGroupFilter(req.user, { _id: mailingId });
const mailingForMosaico = await Mailings.findOneForMosaico(
+ user,
query,
req.user.lang
);
@@ -362,10 +368,6 @@ async function updateMosaico(req, res) {
throw new NotFound(ERROR_CODES.MAILING_NOT_FOUND);
}
- if (!requestHtml) {
- throw new NotFound(ERROR_CODES.MAILING_HTML_MISSING);
- }
-
if (!user.isAdmin) {
const { _workspace, _parentFolder } = mailing;
@@ -386,14 +388,19 @@ async function updateMosaico(req, res) {
mailing.data = req.body.data || mailing.data;
mailing.name =
- modelsUtils.normalizeString(req.body.name) ||
+ modelsUtils.trimString(req.body.name) ||
simpleI18n('default-mailing-name', user.lang);
// http://mongoosejs.com/docs/schematypes.html#mixed
mailing.markModified('data');
- mailing.previewHtml = requestHtml;
+
+ if (requestHtml) {
+ mailing.previewHtml = requestHtml;
+ }
+
await mailing.save();
const mailingForMosaico = await Mailings.findOneForMosaico(
+ user,
query,
req.user.lang
);
diff --git a/packages/server/mailing/mailing.routes.js b/packages/server/mailing/mailing.routes.js
index 6eb15ce9..4927bea7 100644
--- a/packages/server/mailing/mailing.routes.js
+++ b/packages/server/mailing/mailing.routes.js
@@ -27,6 +27,9 @@ router.post(
GUARD_USER,
mailings.downloadZip
);
+
+router.post('/download-multiple-zip', GUARD_USER, mailings.downloadMultipleZip);
+
router.put('/:mailingId/mosaico', GUARD_USER, mailings.updateMosaico);
router.get('/:mailingId/mosaico', GUARD_USER, mailings.readMosaico);
router.post('/:mailingId/duplicate', GUARD_USER, mailings.duplicate);
diff --git a/packages/server/mailing/mailing.schema.js b/packages/server/mailing/mailing.schema.js
index 04e2f975..f0c1155d 100644
--- a/packages/server/mailing/mailing.schema.js
+++ b/packages/server/mailing/mailing.schema.js
@@ -38,7 +38,6 @@ const MailingSchema = Schema(
{
name: {
type: String,
- set: normalizeString,
required: true,
},
previewHtml: {
@@ -115,7 +114,6 @@ MailingSchema.post('find', function () {
MailingSchema.plugin(mongooseHidden, {
hidden: {
- _id: true,
__v: true,
_company: true,
_wireframe: true,
@@ -156,6 +154,51 @@ MailingSchema.statics.findForApi = async function findForApi(query = {}) {
return this.find(query, { previewHtml: 0, data: 0 });
};
+// Use aggregate so we excluse previewHtml and define another boolean variable hasPreviewHtml based on the existence of previewHtml
+MailingSchema.statics.findWithHasPreview = async function findWithHasPreview(
+ query = {}
+) {
+ return this.aggregate([
+ {
+ $match: {
+ ...query,
+ },
+ },
+ {
+ $addFields: {
+ hasHtmlPreview: {
+ $not: [
+ {
+ $not: [
+ {
+ $ifNull: ['$previewHtml', 0],
+ },
+ ],
+ },
+ ],
+ },
+ },
+ },
+ {
+ $project: {
+ id: '$_id',
+ name: 1,
+ group: '$_company',
+ templateName: '$wireframe',
+ templateId: '$_wireframe',
+ userName: '$author',
+ userId: '$_user',
+ _workspace: 1,
+ tags: 1,
+ espIds: 1,
+ hasHtmlPreview: 1,
+ updatedAt: 1,
+ createdAt: 1,
+ },
+ },
+ ]);
+};
+
// Extract used tags from creations
// http://stackoverflow.com/questions/14617379/mongoose-mongodb-count-elements-in-array
MailingSchema.statics.findTags = async function findTags(query = {}) {
@@ -227,6 +270,7 @@ const translations = {
*/
MailingSchema.statics.findOneForMosaico = async function findOneForMosaico(
+ user,
query = {},
lang = 'fr'
) {
@@ -253,9 +297,21 @@ MailingSchema.statics.findOneForMosaico = async function findOneForMosaico(
const mailingId = mailing._id;
const groupId = group._id;
const templateId = mailing._wireframe._id;
+
+ let redirectUrl = null;
+
+ if (user?.isAdmin) {
+ redirectUrl = `/groups/${groupId}?redirectTab=mailings`;
+ } else {
+ redirectUrl = mailing?._parentFolder
+ ? `/?fid=${mailing._parentFolder}`
+ : `/?wid=${mailing._workspace}`;
+ }
+
return {
metadata: {
id: mailingId,
+ groupId: groupId,
templateId,
name: mailing.name,
hasHtmlPreview: !!mailing.previewHtml,
@@ -287,7 +343,7 @@ MailingSchema.statics.findOneForMosaico = async function findOneForMosaico(
placeholder: '/api/images/placeholder/',
},
assets: mailing._wireframe.assets,
- editorIcon: { ...config.brandOptions.editorIcon },
+ editorIcon: { ...config.brandOptions.editorIcon, logoUrl: redirectUrl },
},
titleToken: 'BADSENDER Responsive Email Designer',
// TODO: should be in metadata
diff --git a/packages/server/mailing/mailing.service.js b/packages/server/mailing/mailing.service.js
index 0b97cb6a..7b9f7ac8 100644
--- a/packages/server/mailing/mailing.service.js
+++ b/packages/server/mailing/mailing.service.js
@@ -34,7 +34,6 @@ const processMosaicoHtmlRender = require('../utils/process-mosaico-html-render.j
const Ftp = require('./ftp-client.service.js');
const request = require('request');
-const createError = require('http-errors');
const fileManager = require('../common/file-manage.service.js');
const modelsUtils = require('../utils/model.js');
@@ -47,6 +46,7 @@ const templateService = require('../template/template.service.js');
const folderService = require('../folder/folder.service.js');
const workspaceService = require('../workspace/workspace.service.js');
+const MULTIPLE_DOWNLOAD_ZIP_NAME = 'lepatron';
module.exports = {
createMailing,
findMailings,
@@ -63,9 +63,13 @@ module.exports = {
listMailingForWorkspaceOrFolder,
previewMail,
downloadZip,
+ downloadMultipleZip,
validateMailExist,
processHtmlWithFTPOption,
updateMailEspIds,
+ getMailByMailingIdAndUser,
+ getGroupByCompanyId,
+ getMailNameAndCompanyByMailingIdAndUser,
};
async function listMailingForWorkspaceOrFolder({
@@ -142,13 +146,11 @@ async function updateMailEspIds(mailingId, espId) {
async function findMailings(query) {
const mailingQuery = applyFilters(query);
-
- return Mailings.find(mailingQuery, { previewHtml: 0, data: 0 });
+ return Mailings.findWithHasPreview(mailingQuery);
}
async function findTags(query) {
const mailingQuery = applyFilters(query);
-
return Mailings.findTags(mailingQuery);
}
@@ -254,24 +256,19 @@ async function createMailing(mailing) {
// Process html to the final result state based on ftp
async function processHtmlWithFTPOption({ mailingId, html, user }) {
+ logger.log('Calling processHtmlWithFTPOption');
+ const mailing = await this.getMailByMailingIdAndUser({ mailingId, user });
+
const {
+ prefix,
cdnDownload,
regularDownload,
- prefix,
- ftpEndPointProtocol,
- ftpEndPoint,
- ftpHost,
- ftpPort,
- ftpUsername,
- ftpPassword,
- ftpProtocol,
- ftpPathOnServer,
+ ftpServerParams,
cdnProtocol,
cdnEndPoint,
name,
} = await extractFTPparams({
- mailingId,
- user,
+ mailing,
downloadOptions: {
downLoadForCdn: false,
downLoadForFtp: true,
@@ -292,12 +289,9 @@ async function processHtmlWithFTPOption({ mailingId, html, user }) {
cdnDownload,
regularDownload,
prefix,
- ftpHost,
- ftpPort,
- ftpUsername,
- ftpPassword,
- ftpProtocol,
- ftpPathOnServer,
+ name,
+ ftpServerParams,
+ doesWaitForFtp: true,
});
// Add html with relatives url
const processedHtml = processMosaicoHtmlRender(relativeImagesHtml);
@@ -308,8 +302,7 @@ async function processHtmlWithFTPOption({ mailingId, html, user }) {
cdnDownload,
cdnProtocol,
cdnEndPoint,
- ftpEndPointProtocol,
- ftpEndPoint,
+ ftpServerParams,
processedHtml,
relativesImagesNames,
name,
@@ -330,24 +323,18 @@ async function downloadZip({
throw new InternalServerError(ERROR_CODES.ARCHIVE_IS_NULL);
}
+ const mailing = await this.getMailByMailingIdAndUser({ mailingId, user });
+
const {
cdnDownload,
regularDownload,
prefix,
- ftpEndPointProtocol,
- ftpEndPoint,
- ftpHost,
- ftpPort,
- ftpUsername,
- ftpPassword,
- ftpProtocol,
- ftpPathOnServer,
+ ftpServerParams,
cdnProtocol,
cdnEndPoint,
name,
} = await extractFTPparams({
- mailingId,
- user,
+ mailing,
downloadOptions,
});
@@ -363,13 +350,9 @@ async function downloadZip({
regularDownload,
archive,
prefix,
- ftpHost,
- ftpPort,
- ftpUsername,
- ftpPassword,
- ftpProtocol,
- ftpPathOnServer,
+ ftpServerParams,
name,
+ doesWaitForFtp: false,
});
// ----- HTML
@@ -390,8 +373,7 @@ async function downloadZip({
cdnDownload,
cdnProtocol,
cdnEndPoint,
- ftpEndPointProtocol,
- ftpEndPoint,
+ ftpServerParams,
processedHtml,
relativesImagesNames,
name,
@@ -429,21 +411,203 @@ async function downloadZip({
return { archive: processedImageArchive, name };
}
-// Extract information related to ftp and download state based on mailing id, user and download options
-async function extractFTPparams({ mailingId, user, downloadOptions }) {
+// This function will be used to find out recurrent names inside a array of type Array<{mailing, name}>[]
+function generateUniqueNameFromMailingList({ index, name, accumulator }) {
+ logger.log('Calling generateUniqueNameFromMailingList');
+ // Safe exist for the recurrent function, 30 was choosen because it is not possible for the user download more than 25 zip file at the same time
+ let mailNameToSearch = name;
+ if (index > 25) {
+ throw new InternalServerError(ERROR_CODES.TOO_MUCH_RECURRENT_LOOP);
+ }
+
+ if (index > 0) {
+ mailNameToSearch += ` (${index})`;
+ }
+ const foundMailsWithSameName = accumulator.filter(
+ (mailWithFinalName) => mailWithFinalName.name === mailNameToSearch
+ );
+
+ if (foundMailsWithSameName.length === 0) {
+ return mailNameToSearch;
+ }
+
+ return generateUniqueNameFromMailingList({
+ index: index + 1,
+ name,
+ accumulator,
+ });
+}
+
+// This will handle downloading email as a ZIP file
+async function downloadMultipleZip({
+ mailingIds,
+ archive,
+ downloadOptions,
+ user,
+}) {
+ logger.log('Calling downloadMultipleZip');
+ if (!Array.isArray(mailingIds) || mailingIds.length === 0) {
+ throw new InternalServerError(ERROR_CODES.MAILING_MISSING_SOURCE);
+ }
+
+ if (mailingIds.length === 1) {
+ const firstMailing = await this.findOne(mailingIds[0]);
+
+ return this.downloadZip({
+ mailingId: firstMailing._id,
+ html: firstMailing.previewHtml,
+ archive,
+ user,
+ downloadOptions,
+ });
+ }
+
+ const mailingsWithNamePromises = mailingIds.map(async (mailingId) => {
+ const mailing = await this.getMailByMailingIdAndUser({ mailingId, user });
+ return {
+ mailing: mailing,
+ name: mailing.name,
+ };
+ });
+
+ const mailingsWithNames = await Promise.all(mailingsWithNamePromises);
+
+ const mailingsWithUniqueNames = mailingsWithNames.reduce(
+ (accumulator, currentValue) => {
+ const index = 0;
+ const uniqueMailName = generateUniqueNameFromMailingList({
+ index,
+ name: currentValue.name,
+ accumulator,
+ });
+ accumulator.push({
+ name: uniqueMailName,
+ mailing: currentValue.mailing,
+ });
+ return accumulator;
+ },
+ []
+ );
+
+ if (!archive) {
+ throw new InternalServerError(ERROR_CODES.ARCHIVE_IS_NULL);
+ }
+
+ if (
+ !Array.isArray(mailingsWithUniqueNames) ||
+ mailingsWithUniqueNames.length === 0
+ ) {
+ return;
+ }
+
+ for (const mailWithUniqueName of mailingsWithUniqueNames) {
+ const { mailing, name: uniqueName } = mailWithUniqueName;
+ const {
+ cdnDownload,
+ regularDownload,
+ prefix,
+ ftpServerParams,
+ cdnProtocol,
+ cdnEndPoint,
+ name,
+ } = await extractFTPparams({
+ mailing,
+ parentContainer: MULTIPLE_DOWNLOAD_ZIP_NAME,
+ overrideMailName: uniqueName,
+ user,
+ downloadOptions,
+ });
+
+ const {
+ relativesImagesNames,
+ html: relativeImagesHtml,
+ } = await handleRelativeOrFtpImages({
+ cdnDownload,
+ regularDownload,
+ html: mailing.previewHtml,
+ archive,
+ prefix,
+ ftpServerParams,
+ name,
+ doesWaitForFtp: false,
+ });
+
+ const processedHtml = processMosaicoHtmlRender(relativeImagesHtml);
+
+ if (regularDownload) {
+ archive.append(processedHtml, {
+ prefix,
+ name: `${name}.html`,
+ });
+ } else {
+ const {
+ htmlProcessedWithFtp,
+ } = await replaceImageWithFTPEndpointBaseInProcessedHtml({
+ cdnDownload,
+ cdnProtocol,
+ cdnEndPoint,
+ ftpServerParams,
+ processedHtml,
+ relativesImagesNames,
+ name,
+ });
+ // archive
+
+ archive.append(htmlProcessedWithFtp, {
+ prefix,
+ name: `${name}.html`,
+ });
+ }
+ }
+
+ return { archive, name: MULTIPLE_DOWNLOAD_ZIP_NAME };
+}
+
+async function getMailByMailingIdAndUser({ mailingId, user }) {
const query = modelsUtils.addGroupFilter(user, { _id: mailingId });
// mailing can come without group if created by the admin
// • in order to retrieve the group, look at the wireframe
const mailing = await Mailings.findOne(query)
- .select({ _wireframe: 1, name: 1 })
+ .select({ data: 0 })
.populate('_wireframe');
- if (!mailing) throw new createError.NotFound();
+ if (!mailing) throw new NotFound(ERROR_CODES.MAILING_MISSING_SOURCE);
+ return mailing;
+}
+
+async function getMailNameAndCompanyByMailingIdAndUser({ mailingId, user }) {
+ const query = modelsUtils.addGroupFilter(user, { _id: mailingId });
+ const mailing = await Mailings.findOne(query)
+ .select({ name: 1, _company: 1 })
+ .lean();
+ if (!mailing) throw new NotFound(ERROR_CODES.MAILING_MISSING_SOURCE);
+ return mailing;
+}
+
+async function getGroupByCompanyId({ companyId }) {
+ const group = await Groups.findById(companyId).lean();
+ if (!group) throw new NotFound(ERROR_CODES.GROUP_NOT_FOUND);
+ return group;
+}
+// Extract information related to ftp and download state based on mailing id, user and download options
+async function extractFTPparams({
+ mailing,
+ downloadOptions,
+ parentContainer = null,
+ overrideMailName = null,
+}) {
+ console.log('Calling extract ftp params');
+
+ if (!mailing || !mailing?._wireframe?._company || !mailing.name)
+ throw new NotFound(ERROR_CODES.MAILING_MISSING_SOURCE);
// if (!isFromCompany(user, mailing._company)) throw new createError.Unauthorized()
// group is needed to check zip format & DL configuration
- const group = await Groups.findById(mailing._wireframe._company).lean();
- if (!group) throw new createError.NotFound();
+
+ const group = await getGroupByCompanyId({
+ companyId: mailing?._wireframe?._company,
+ });
+ if (!group) throw new NotFound(ERROR_CODES.GROUP_NOT_FOUND);
const {
downloadMailingWithoutEnclosingFolder,
@@ -478,11 +642,13 @@ async function extractFTPparams({ mailingId, user, downloadOptions }) {
throw new InternalServerError(ERROR_CODES.FTP_NOT_DEFINED_FOR_GROUP);
}
- const name = getName(mailing.name);
+ const name = getName(overrideMailName ?? mailing.name);
// prefix is `zip-stream` file prefix => our enclosing folder ^_^
// !WARNING default mac unzip will always put it in an folder if more than 1 file
// => test with The Unarchiver
- const prefix = downloadMailingWithoutEnclosingFolder ? '' : `${name}/`;
+ const prefix = downloadMailingWithoutEnclosingFolder
+ ? ''
+ : `${!parentContainer ? '' : `${parentContainer}/`}${name}/`;
// const $ = cheerio.load(html)
@@ -497,14 +663,16 @@ async function extractFTPparams({ mailingId, user, downloadOptions }) {
cdnDownload,
regularDownload,
prefix,
- ftpEndPointProtocol,
- ftpEndPoint,
- ftpHost,
- ftpPort,
- ftpUsername,
- ftpPassword,
- ftpProtocol,
- ftpPathOnServer,
+ ftpServerParams: {
+ ftpEndPointProtocol,
+ ftpEndPoint,
+ ftpHost,
+ ftpPort,
+ ftpUsername,
+ ftpPassword,
+ ftpProtocol,
+ ftpPathOnServer,
+ },
cdnProtocol,
cdnEndPoint,
name,
@@ -515,12 +683,12 @@ async function replaceImageWithFTPEndpointBaseInProcessedHtml({
cdnDownload,
cdnProtocol,
cdnEndPoint,
- ftpEndPointProtocol,
- ftpEndPoint,
+ ftpServerParams,
processedHtml,
relativesImagesNames,
name,
}) {
+ const { ftpEndPointProtocol, ftpEndPoint } = ftpServerParams;
const endpointBase = cdnDownload
? `${cdnProtocol}${cdnEndPoint}`
: `${ftpEndPointProtocol}${ftpEndPoint}`;
@@ -548,14 +716,18 @@ async function handleRelativeOrFtpImages({
regularDownload,
archive,
prefix,
- ftpHost,
- ftpPort,
- ftpUsername,
- ftpPassword,
- ftpProtocol,
- ftpPathOnServer,
+ ftpServerParams,
name,
+ doesWaitForFtp,
}) {
+ const {
+ ftpHost,
+ ftpPort,
+ ftpUsername,
+ ftpPassword,
+ ftpProtocol,
+ ftpPathOnServer,
+ } = ftpServerParams;
if (!html) {
throw new InternalServerError(ERROR_CODES.HTML_IS_NULL);
}
@@ -664,8 +836,11 @@ async function handleRelativeOrFtpImages({
ftpPathOnServer +
(ftpPathOnServer.substr(ftpPathOnServer.length - 1) === '/' ? '' : '/') +
`${name}/`;
-
- await ftpClient.upload(allImages, folderPath);
+ if (doesWaitForFtp) {
+ await ftpClient.upload(allImages, folderPath);
+ } else {
+ ftpClient.upload(allImages, folderPath);
+ }
}
return { relativesImagesNames, archive, html };
@@ -808,7 +983,10 @@ async function deleteMailing({ mailingId, workspaceId, parentFolderId, user }) {
return deleteResponse;
}
+
async function moveMailing(user, mailing, workspaceId, parentFolderId) {
+ console.log('Calling moveMailing');
+
checkEitherWorkspaceOrFolderDefined(workspaceId, parentFolderId);
let sourceWorkspace;
let destinationWorkspace;
@@ -953,12 +1131,14 @@ function applyFilters(query) {
if (query.workspaceId) {
return {
...mailingQueryStrictGroup,
- _workspace: query.workspaceId,
+ _workspace: mongoose.Types.ObjectId(query.workspaceId),
};
- } else {
+ } else if (query.parentFolderId) {
return {
...mailingQueryStrictGroup,
- _parentFolder: query.parentFolderId,
+ _parentFolder: mongoose.Types.ObjectId(query.parentFolderId),
};
+ } else {
+ return mailingQueryStrictGroup;
}
}
diff --git a/packages/server/mailing/mosaico-editor.controller.js b/packages/server/mailing/mosaico-editor.controller.js
index eb32308a..da28156e 100644
--- a/packages/server/mailing/mosaico-editor.controller.js
+++ b/packages/server/mailing/mosaico-editor.controller.js
@@ -36,6 +36,7 @@ async function render(req, res) {
const query = modelsUtils.addGroupFilter(req.user, { _id: mailingId });
const mailingForMosaico = await Mailings.findOneForMosaico(
+ user,
query,
user.lang
);
diff --git a/packages/server/mailing/send-test-mail.controller.js b/packages/server/mailing/send-test-mail.controller.js
index cf6d714c..e0803772 100644
--- a/packages/server/mailing/send-test-mail.controller.js
+++ b/packages/server/mailing/send-test-mail.controller.js
@@ -1,16 +1,8 @@
'use strict';
-const { createError, BadRequest } = require('http-errors');
const asyncHandler = require('express-async-handler');
-const config = require('../node.config.js');
-const { Mailings } = require('../common/models.common.js');
-const mail = require('../mailing/mail.service.js');
-const modelsUtils = require('../utils/model.js');
-const processMosaicoHtmlRender = require('../utils/process-mosaico-html-render.js');
-const isEmail = require('validator/lib/isEmail');
-const ERROR_CODES = require('../constant/error-codes.js');
-const logger = require('../utils/logger.js');
+const sendTestMailService = require('./send-test-mail.service');
module.exports = asyncHandler(sendTestMail);
@@ -23,6 +15,7 @@ module.exports = asyncHandler(sendTestMail);
* @apiParam {string} mailingId
*
* @apiParam (Body) {String} rcpt the recipient email address
+ * @apiParam (Body) {String} emailGroupId The id of the emails group
* @apiParam (Body) {String} html the HTML output get in mosaico by `viewModel.exportHTML()`
*
* @apiSuccess {String} mailingList the emails to which the mailing has been sent
@@ -30,35 +23,19 @@ module.exports = asyncHandler(sendTestMail);
*/
async function sendTestMail(req, res) {
- const { user, body } = req;
+ const {
+ user,
+ body: { html: htmlFromEditor, rcpt, emailsGroupId },
+ } = req;
const { mailingId } = req.params;
- const query = modelsUtils.addGroupFilter(req.user, { _id: mailingId });
- const mailing = await Mailings.findOne(query)
- .select({ name: 1, _company: 1 })
- .lean();
- if (!mailing) throw new createError.NotFound();
- // TODO: add back group check
- // if (!isFromCompany(user, mailing._company)) throw new createError.Unauthorized()
- // body.html is the result of viewModel.exportHTML()
- // • in /src/js/ext/badsender-server-storage.js
- const html = processMosaicoHtmlRender(req.body.html);
+ const resultSendingTestMail = await sendTestMailService.sendTestMail({
+ user,
+ rcpt,
+ htmlFromEditor,
+ emailsGroupId,
+ mailingId,
+ });
- const adresses = body.rcpt.split(';');
- for (const mail of adresses) {
- if (!isEmail(mail)) {
- throw new BadRequest(ERROR_CODES.EMAIL_NOT_VALID);
- }
- }
- for (const address of adresses) {
- const mailInfo = await mail.send({
- to: address,
- replyTo: user?.email,
- subject: config.emailOptions.testSubjectPrefix + mailing.name,
- html,
- });
-
- logger.log('Message sent: ', mailInfo.response);
- }
- res.json({ mailingList: body.rcpt });
+ res.json({ result: resultSendingTestMail });
}
diff --git a/packages/server/mailing/send-test-mail.service.js b/packages/server/mailing/send-test-mail.service.js
new file mode 100644
index 00000000..0fc95ece
--- /dev/null
+++ b/packages/server/mailing/send-test-mail.service.js
@@ -0,0 +1,76 @@
+'use strict';
+
+const config = require('../node.config.js');
+const isEmail = require('validator/lib/isEmail');
+const ERROR_CODES = require('../constant/error-codes.js');
+const logger = require('../utils/logger.js');
+const { onlyUnique } = require('../utils/array.js');
+const mailingService = require('./mailing.service.js');
+const emailsGroupService = require('../emails-group/emails-group.service');
+const processMosaicoHtmlRender = require('../utils/process-mosaico-html-render.js');
+
+const { BadRequest } = require('http-errors');
+const mail = require('../mailing/mail.service.js');
+
+module.exports = {
+ sendTestMail,
+};
+
+async function sendTestMail({
+ mailingId,
+ emailsGroupId,
+ rcpt,
+ htmlFromEditor,
+ user,
+}) {
+ const mailing = await mailingService.getMailNameAndCompanyByMailingIdAndUser({
+ mailingId,
+ user,
+ });
+
+ // TODO: add back group check
+ // if (!isFromCompany(user, mailing._company)) throw new createError.Unauthorized()
+
+ // body.html is the result of viewModel.exportHTML()
+ // • in /src/js/ext/badsender-server-storage.js
+ const html = processMosaicoHtmlRender(htmlFromEditor);
+
+ let adresses = rcpt.split(';');
+
+ if (!!emailsGroupId && typeof emailsGroupId !== 'undefined') {
+ const emailGroups = await emailsGroupService.getEmailsGroup({
+ userGroupId: user.group?.id,
+ emailsGroupId,
+ });
+
+ if (emailGroups && emailGroups.emails) {
+ adresses = [...adresses, ...emailGroups.emails.split(';')];
+ }
+ }
+
+ adresses = adresses.filter(onlyUnique).filter((email) => !!email);
+
+ for (const mail of adresses) {
+ if (!isEmail(mail)) {
+ throw new BadRequest(ERROR_CODES.EMAIL_NOT_VALID);
+ }
+ }
+
+ for (const address of adresses) {
+ try {
+ const mailInfo = await mail.send({
+ to: address,
+ replyTo: user?.email,
+ subject: config.emailOptions.testSubjectPrefix + mailing.name,
+ html,
+ });
+
+ logger.log('Message sent: ', mailInfo.response);
+ } catch (error) {
+ logger.error('Error occured while sent email: ', address);
+ logger.error('Error ', error.message);
+ }
+ }
+
+ return adresses;
+}
diff --git a/packages/server/node.config.js b/packages/server/node.config.js
index ffbe5a15..0243ff12 100644
--- a/packages/server/node.config.js
+++ b/packages/server/node.config.js
@@ -42,7 +42,7 @@ const config = rc('lepatron', {
},
emailOptions: {
from: 'LePatron.email local test
+
+
+
+
+ {{ modalText }} + +
+
diff --git a/packages/ui/routes/mailings/__partials/mailings-table.vue b/packages/ui/routes/mailings/__partials/mailings-table.vue
index e6f68e51..6c780061 100644
--- a/packages/ui/routes/mailings/__partials/mailings-table.vue
+++ b/packages/ui/routes/mailings/__partials/mailings-table.vue
@@ -36,6 +36,8 @@ const TABLE_ACTIONS = [
ACTIONS.COPY_MAIL,
ACTIONS.MOVE_MAIL,
ACTIONS.PREVIEW,
+ ACTIONS.DOWNLOAD,
+ ACTIONS.DOWNLOAD_FTP,
'actionMoveMail',
];
@@ -57,6 +59,7 @@ export default {
mailings: { type: Array, default: () => [] },
mailingsSelection: { type: Array, default: () => [] },
tags: { type: Array, default: () => [] },
+ hasFtpAccess: { type: Boolean, default: false },
},
data() {
return {
@@ -339,6 +342,12 @@ export default {
tags,
});
},
+ async handleDownloadMail({ mailing, isWithFtp }) {
+ this.$emit('on-single-mail-download', {
+ mailing,
+ isWithFtp,
+ });
+ },
},
};
@@ -429,6 +438,26 @@ export default {
>
{{ $t(actionsDetails[actions.DELETE].text) }}
+ Fake image editor ': '', + // download button 'dl-btn-regular': 'Standard download', 'dl-btn-cdn': 'zip with CDN', + // Crop interface 'rotate-left': 'Rotate left', 'rotate-right': 'Rotate right', 'vertical-mirror': 'Vertical mirror', 'horizontal-mirror': 'Horizontal mirror', - 'error-server': 'An error has occured while calling server api', + 'error-server': 'An error has occured while calling server API :(', cancel: 'CANCEL', upload: 'UPLOAD', @@ -47,31 +55,45 @@ module.exports = { 'sender-mail': 'Sender email address', replyto: 'Reply email address', mailSubject: 'Subject', - templateSubject: 'Subject', + templateSubject: 'Template subject', name: 'Name', - mailName: 'Mail name', + mailName: 'Email name', + templateName: 'Template name', 'export-to': 'Export to', - exporting: 'Exporting ...', + exporting: 'Exporting…', loading: 'Loading', submit: 'Submit', - export: 'Export', - close: 'Close', + close: 'CANCEL', + export: 'EXPORT', + 'warning-esp-message': 'Any update will replace the email in your ESP', + // profile form validation - 'warning-esp-message': - 'Any update will override the mail campaign in the email service provider.', - 'mail-name-required': 'Mail name is required', + 'mail-name-required': 'Email name is required', 'template-name-required': 'Template name is required', - 'mail-subject-required': 'Subject is required', - 'template-subject-required': 'Subject is required', - 'mail-success-esp-send': 'Mail exported successfully', + 'mail-subject-required': 'Email subject is required', + 'template-subject-required': 'Template subject is required', + 'mail-success-esp-send': 'Email exported successfully', 'template-success-esp-send': 'Template exported successfully', 'error-server-400': - 'Esp parameters invalids, check if sender mail matches API key.', - 'error-server-402': 'Export fail, provider require payment', - 'error-server-409': 'Mail name already used', - 'error-server-500': 'An error has occured while calling server api', + 'ESP parameters invalids. Check if sender email matches API key', + 'error-server-402': 'Export fail, provider require payment.', + 'error-server-409': 'Email name already used', + 'error-server-500': 'An error has occured while calling server API :(', 'supported-language': 'Supported language', 'target-table': 'Target table', 'encoding-type': 'Encoding type', entity: 'Entity', + + // test list + 'title-send-test-mails': 'Send a test email', + 'send-test-success': 'Email sent successfully', + 'send-test-error': 'An error has occured while sending the emai :(l', + 'placeholder-input-emails-test': + 'Example: firstemail@test.com;secondemail@test.com', + 'emails-test': 'Enter one or more emails', + 'emails-invalid': + 'The email addresses entered are invalid. Please separate email addresses with ";"', + 'placeholder-emails-groups': 'Select a list', + 'sending-test-mails': 'Sending test mail…', + 'send-test-mails': 'Send', }; diff --git a/public/lang/badsender-fr.js b/public/lang/badsender-fr.js index 39fcf20e..30810d2e 100644 --- a/public/lang/badsender-fr.js +++ b/public/lang/badsender-fr.js @@ -2,79 +2,100 @@ module.exports = { // edit title - 'edit-title-double-click': 'Double-cliquer pour éditer', - 'edit-title-cancel': 'Annuler l\'édition', + 'edit-title-double-click': 'Double-cliquer pour modifier', + 'edit-title-cancel': 'Annuler la modification', 'edit-title-save': 'Enregistrer le nouveau nom', 'edit-title-ajax-pending': 'Changement du nom…', 'edit-title-ajax-success': 'Mise à jour du nom effectuée', - 'edit-title-ajax-fail': 'Impossible d\'enregistrer le nouveau nom', + 'edit-title-ajax-fail': "Impossible d'enregistrer le nouveau nom :(", + // empty title fallback 'title-empty': 'sans titre', - // - 'save-message-success': 'L\'email a été sauvé', - 'save-message-error': 'Une erreur est survenue lors de l\'enregistrement', - // + + // save + 'save-message-success': "L'email a été sauvegardé", + 'save-message-error': "Une erreur est survenue lors de l'enregistrement :(", + + // gallery 'gallery-title': 'Galeries :', - 'gallery-mailing': 'SPÉCIFIQUE À L\'EMAIL', - 'gallery-mailing-loading': 'Chargement de la galerie de l\'email…', - 'gallery-mailing-empty': 'La galerie de l\'email est vide', + 'gallery-mailing': "SPÉCIFIQUE À L'EMAIL", + 'gallery-mailing-loading': "Chargement de la galerie de l'email…", + 'gallery-mailing-empty': "La galerie de l'email est vide", 'gallery-template': 'COMMUN AU TEMPLATE', 'gallery-template-loading': 'Chargement de la galerie du template…', 'gallery-template-empty': 'La galerie du template est vide', - 'gallery-remove-image-success': 'L\'image a bien été enlevée de la galerie', + 'gallery-remove-image-success': "L'image a bien été supprimée de la galerie", 'gallery-remove-image-fail': - 'Une erreur est survenue lors de la suppression de l\'image', + "Une erreur est survenue lors de la suppression de l'image :(", + // bgimage widget - 'widget-bgimage-button': 'choisissez une image', - 'widget-bgimage-reset': 'enlever l\'image', + 'widget-bgimage-button': 'Choisissez une image', + 'widget-bgimage-reset': "Enlever l'image", + // prevent i18n console.warn 'Fake image editor': '', 'Fake image editor ': '', + // download button 'dl-btn-regular': 'Téléchargement local', 'dl-btn-cdn': 'images ICOU', + // Crop interface - 'rotate-left': 'Incliner à gauche', - 'rotate-right': 'Incliner à droite', + 'rotate-left': 'Tourner vers la gauche', + 'rotate-right': 'Tourner vers la droite', 'vertical-mirror': 'Miroir vertical', 'horizontal-mirror': 'Miroir horizontal', 'error-server': - 'Une erreur s\'est produite lors de l\'appel de l\'API du serveur', + "Une erreur s'est produite lors de l'appel de l'API du serveur", cancel: 'ANNULER', - upload: 'UPLOADER', + upload: 'ENVOYER', // Profile form esp - 'sender-name': 'Nom de l\'expéditeur', - 'sender-mail': 'Adresse email de l\'expéditeur', + 'sender-name': "Nom de l'expéditeur", + 'sender-mail': "Adresse email de l'expéditeur", replyto: 'Adresse email de réponse', - mailSubject: 'Objet du mail', + mailSubject: "Objet de l'email", templateSubject: 'Objet du template', name: 'Nom', - mailName: 'Nom du mail', + mailName: "Nom de l'email", templateName: 'Nom du template', 'export-to': 'Exporter vers', - exporting: 'Exportation en cours ...', - loading: 'Loading', + exporting: 'Exportation…', + loading: 'Chargement', submit: 'Enregistrer', - close: 'Fermer', - export: 'Exporter', + close: 'ANNULER', + export: 'EXPORTER', 'warning-esp-message': - 'Toute mise à jour remplacera le mail dans le fournisseur de services de messagerie', + "Toute mise à jour remplacera l'email dans votre routeur", + // profile form validation - 'mail-name-required': 'Le nom du mail est requis', - 'template-name-required': 'Le nom du template est requis', - 'mail-subject-required': 'L\'Objet du mail est requis', - 'template-subject-required': 'L\'Objet du template est requis', - 'mail-success-esp-send': 'Mail exporté avec succès', + 'mail-name-required': "Veuillez saisir un nom pour l'email", + 'template-name-required': 'Veuillez saisir un nom pour le template', + 'mail-subject-required': "Veuillez saisir l'objet de l'email", + 'template-subject-required': "euillez saisir l'objet du template", + 'mail-success-esp-send': 'Email exporté avec succès', 'template-success-esp-send': 'Template exporté avec succès', 'error-server-400': - 'Paramètres Esp invalides, vérifiez si le courrier de l\'expéditeur correspond à la clé API', - 'error-server-402': 'Échec de l\'exportation, l\'ESP exige un paiement', - 'error-server-409': 'Nom de mail déjà utilisé', + "Paramètres ESP invalides. Vérifiez si l'adresse email de l'expéditeur correspond à la clé API", + 'error-server-402': "Échec de l'exportation. L'ESP exige un paiement.", + 'error-server-409': "Nom d'email déjà utilisé", 'error-server-500': - 'Une erreur s\'est produite lors de l\'appel de l\'API du serveur', + "Une erreur s'est produite lors de l'appel de l'API du serveur :(", 'supported-language': 'Langue', 'target-table': 'Table cible', - 'encoding-type': 'Type d\'encodage', + 'encoding-type': "Type d'encodage", entity: 'Entité', + + // test list + 'title-send-test-mails': 'Envoyer un email de test', + 'send-test-success': 'Email envoyé avec succès', + 'send-test-error': "Erreur lors de l'envoi de l'email :(", + 'placeholder-input-emails-test': + 'Exemple : premieremail@test.com;secondemail@test.com', + 'emails-test': 'Saisir un ou plusieurs emails', + 'emails-invalid': + 'Les adresses emails saisis sont invalides. Veuillez séparer les adresses emails par ";"', + 'placeholder-emails-groups': 'Sélectionnez une liste', + 'sending-test-mails': "Envoi de l'email de test…", + 'send-test-mails': 'Envoyer', }; diff --git a/yarn.lock b/yarn.lock index 09e1c33d..af9e9769 100644 --- a/yarn.lock +++ b/yarn.lock @@ -19268,6 +19268,11 @@ vue-router@^3.4.5, vue-router@^3.4.9: resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.5.2.tgz#5f55e3f251970e36c3e8d88a7cd2d67a350ade5c" integrity sha512-807gn82hTnjCYGrnF3eNmIw/dk7/GE4B5h69BlyCK9KHASwSloD1Sjcn06zg9fVG4fYH2DrsNBZkpLtb25WtaQ== +vue-select@3.13.0: + version "3.13.0" + resolved "https://registry.yarnpkg.com/vue-select/-/vue-select-3.13.0.tgz#78d519984139156adcbf56af260fb6c2a69093a6" + integrity sha512-+PcWtfA1i3WVtkVwBPQknnOZL6QWSD2XiAVbSn0xAQvjOrmGAg7z+o9HkezXhtGdEstloJOdM8SujrUVyphKqg== + vue-server-renderer@^2.6.10, vue-server-renderer@^2.6.12: version "2.6.14" resolved "https://registry.yarnpkg.com/vue-server-renderer/-/vue-server-renderer-2.6.14.tgz#c8bffff152df6b47b858818ef8d524d2fc351654" |