Merge branch 'master' into development

This commit is contained in:
Tomas Bures 2018-08-06 21:02:41 +05:30
commit 877e0a857d
44 changed files with 5260 additions and 366 deletions

View file

@ -12,7 +12,7 @@
* Email templates
* Large CSV list import files
Subscribe to Mailtrain Newsletter [here](http://mailtrain.org/subscription/EysIv8sAx) (uses Mailtrain obviously)
Subscribe to Mailtrain Newsletter [here](https://mailtrain.org/subscription/S18sew2wM) (uses Mailtrain obviously)
## Hardware Requirements
* 1 vCPU
@ -30,6 +30,7 @@ Depending on how you have configured your system and Docker you may need to prep
* Download Mailtrain files using git: `git clone git://github.com/Mailtrain-org/mailtrain.git` (or download [zipped repo](https://github.com/Mailtrain-org/mailtrain/archive/master.zip)) and open Mailtrain folder `cd mailtrain`
* Copy the file `docker-compose.override.yml.tmpl` to `docker-compose.override.yml` and modify it if you need to.
* Bring up the stack with: `docker-compose up -d`
* Start: `docker-compose start`
* Open [http://localhost:3000/](http://localhost:3000/) (change the host name `localhost` to the name of the host where you are deploying the system).
* Authenticate as user `admin` with password `test`
* Navigate to [http://localhost:3000/settings](http://localhost:3000/settings) and update service configuration.
@ -58,4 +59,4 @@ For more information, please [read the docs](http://docs.mailtrain.org/).
* Versions 1.22.0 and up **GPL-V3.0**
* Versions 1.21.0 and up: **EUPL-1.1**
* Versions 1.19.0 and up: **MIT**
* Up to versions 1.18.0 **GPL-V3.0**
* Up to versions 1.18.0 **GPL-V3.0**

Binary file not shown.

View file

@ -3,14 +3,14 @@ msgstr ""
"Project-Id-Version: \n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-05-04 00:45+0200\n"
"PO-Revision-Date: 2017-05-04 00:46+0200\n"
"PO-Revision-Date: 2018-03-07 14:12+0100\n"
"Last-Translator: \n"
"Language-Team: \n"
"Language: de_DE\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 2.0.1\n"
"X-Generator: Poedit 2.0.6\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: views/archive/layout.hbs:1 views/layout.hbs:1
@ -872,7 +872,7 @@ msgstr "Hallo [FIRST_NAME/Customer],"
#: views/emails/stationery-html.hbs:2 views/emails/stationery-text.hbs:2
msgid "Cheers,"
msgstr "Viele Grüsse,"
msgstr "Viele Grüße,"
#: views/index.hbs:1
msgid "List Management"
@ -918,16 +918,16 @@ msgstr "Mailtrain bietet auch benutzerdefinierte Formulare."
#: views/index.hbs:8
msgid "List Segmentation"
msgstr "Listen Segementierung"
msgstr "Segmentierung"
#: views/index.hbs:9
msgid ""
"Send messages only to list subscribers that match predefined segmentation "
"rules. No need to create separate lists with small differences."
msgstr ""
"Senden Sie nur Nachrichten an Abonnenten welche die vordefinierten "
"Segmentierungsregeln erfüllen. Keine Notwendigkeit, separate Listen mit "
"kleinen Unterschieden zu erstellen."
"Senden Sie nur Nachrichten an Abonnenten, welche die vordefinierten "
"Segmentierungsregeln erfüllen. Es besteht keine Notwendigkeit, separate "
"Listen mit kleinen Unterschieden zu erstellen."
#: views/index.hbs:11
msgid "RSS Campaigns"
@ -954,7 +954,7 @@ msgid ""
msgstr ""
"Wenn für eine Liste ein benutzerdefiniertes Feld für den GPG-Public-Key "
"vorhanden ist, können Abonnenten ihren GPG-Public-Key hochladen, um "
"verschlüsselte E-Mails dieser der Liste zu empfangen."
"verschlüsselte E-Mails der Liste zu empfangen."
#: views/index.hbs:17
msgid "Click Stats"
@ -984,7 +984,7 @@ msgstr ""
#: views/index.hbs:23
msgid "Send via Any Provider"
msgstr "Sende mit "
msgstr "Sende mit"
#: views/index.hbs:24
msgid ""
@ -1121,7 +1121,7 @@ msgstr "Liste erstellen"
#: views/lists/create.hbs:6 views/lists/edit.hbs:7
msgid "List Name"
msgstr "Linstennamen"
msgstr "Name der Liste"
#: views/lists/create.hbs:9 views/lists/edit.hbs:15
msgid "Allow public users to subscribe themselves"
@ -1472,7 +1472,7 @@ msgstr "ID"
#: views/lists/lists.hbs:7 views/reports/partials/report-fields.hbs:10
msgid "Subscribers"
msgstr "Abonnenten "
msgstr "Abonnenten"
#: views/lists/segments/create.hbs:3 views/lists/segments/edit.hbs:3
#: views/lists/segments/rule-configure.hbs:3
@ -1966,7 +1966,7 @@ msgstr "Mosaico öffnen"
#: views/partials/plaintext.hbs:1
msgid "Template content (plaintext)"
msgstr "Vorlagen-Inhalt (Klartext) "
msgstr "Vorlagen-Inhalt (Klartext)"
#: views/report-templates/create.hbs:2 views/report-templates/edit.hbs:2
#: views/report-templates/report-templates.hbs:2
@ -2386,7 +2386,7 @@ msgstr "VERP verwenden um bounces zu erfassen"
#: views/settings.hbs:71
msgid "Server hostname"
msgstr "Server Hostname"
msgstr "Hostname"
#: views/settings.hbs:72
msgid "The VERP server hostname, eg. bounces.example.com"
@ -2462,7 +2462,7 @@ msgstr ""
#: views/settings.hbs:84
msgid "DKIM Signing by ZoneMTA"
msgstr "DKIM Signing by ZoneMTA"
msgstr "DKIM Signing durch ZoneMTA"
#: views/settings.hbs:85
msgid ""
@ -2507,7 +2507,7 @@ msgstr "DKIM Domain"
#: views/settings.hbs:91
msgid "Domain name for the DKIM key"
msgstr "Domain-Name für den DKIM Key"
msgstr "Domain-Name für den DKIM-Key"
#: views/settings.hbs:92
msgid "Leave blank to use the sender email address domain"
@ -2582,7 +2582,7 @@ msgstr "Ihr Abonnement für unsere Liste wurde bestätigt"
#: views/subscription/mail-subscription-confirmed-html.mjml.hbs:3
msgid "If you want to modify your subscription then you can "
msgstr "Wenn Sie Ihr Abonnement ändern möchten, dann können Sie"
msgstr "Wenn Sie Ihr Abonnement ändern möchten, dann können Sie "
#: views/subscription/mail-subscription-confirmed-html.mjml.hbs:4
#: views/subscription/mail-subscription-confirmed-text.hbs:4
@ -2731,7 +2731,7 @@ msgstr "Einstellungen aktualisieren"
#: views/subscription/web-subscribe.mjml.hbs:1
msgid "Subscribe to List"
msgstr "Diese Liste Abonnieren"
msgstr "Diese Liste abonnieren"
#: views/subscription/web-subscribed.mjml.hbs:3
msgid "Thank you for subscribing!"
@ -2900,7 +2900,7 @@ msgstr "Ihre E-Mail-Adresse"
#: views/users/account.hbs:9
msgid ""
"This address is used for account recovery in case you loose your password"
"This address is used for account recovery in case you lose your password"
msgstr ""
"Diese Adresse wird für die Wiederherstellung des Kontos verwendet, falls Sie "
"Ihr Passwort vergessen haben"
@ -3185,7 +3185,7 @@ msgstr "Wähle Sie Ihr neues Passwort"
#: views/users/reset.hbs:5
msgid "Please enter a new password."
msgstr "Bitte geben Sie ein neues Passwort ein"
msgstr "Bitte geben Sie ein neues Passwort ein."
#: lib/editor-helpers.js:17 routes/templates.js:95
msgid "Could not find template with specified ID"
@ -3488,11 +3488,11 @@ msgid "Could not save subscription"
msgstr "Abonnement konnte nicht gespeichert werden"
#: lib/models/subscriptions.js:441 lib/models/subscriptions.js:471
msgid "Missing Subbscription ID"
msgid "Missing Subscription ID"
msgstr "Abonnement-ID fehlt"
#: lib/models/subscriptions.js:499
msgid "Missing Subbscription email address"
msgid "Missing Subscription email address"
msgstr "Abonnement-E-Mail-Adresse fehlt"
#: lib/models/subscriptions.js:578 lib/models/subscriptions.js:827
@ -3650,6 +3650,22 @@ msgstr "Eingeloggt als %s"
msgid "Incorrect username or password"
msgstr "Falscher Benutzername oder Passwort"
#: lib/subscription-mail-helpers.js:39
msgid "%s: Email Address Already Registered"
msgstr "%s: Email-Adresse bereits registriert"
#: lib/subscription-mail-helpers.js:49
msgid "%s: Please Confirm Email Change in Subscription"
msgstr "%s: Bitte bestätigen Sie die Änderung der Email-Adresse"
#: lib/subscription-mail-helpers.js:69
msgid "%s: Please Confirm Unsubscription"
msgstr "%s: Bitte bestätigen Sie die Kündigung des Abonnements"
#: lib/subscription-mail-helpers.js:76
msgid "%s: Unsubscription Confirmed"
msgstr "%s: Kündigung des Abonnements bestätigt"
#: lib/tools.js:148
msgid "Blocked email address \"%s\""
msgstr "Gesperrte E-Mail-Adresse \"%s\""
@ -4593,3 +4609,31 @@ msgstr "Unbekannter Trigger-Typ %s"
#~ msgid "Yes, subscribe me to this list:"
#~ msgstr "Ja, tragen Sie mich in diese Liste ein:"
#: lib/models/subscriptions.js:910 routes/subscription.js:472
#: routes/subscription.js:544 routes/subscription.js:580
#: routes/subscription.js:696 routes/subscription.js:771
msgid "Subscription not found in this list"
msgstr "Das Abonnement wurde in dieser Liste nicht gefunden"
#: views/subscription/web-confirm-unsubscription-notice.mjml.hbs:2
msgid ""
"We need to confirm your email address. To complete the unsubscription "
"process, please click the link in the email we just sent you."
msgstr ""
"Wir müssen Ihre E-Mail-Adresse bestätigen. Um die Kündigung Ihres Abonnements abzuschließen, "
"klicken Sie bitte auf den Link in der E-Mail, die wir Ihnen gerade geschickt "
"haben."
msgid "Please Confirm Unsubscription"
msgstr "Bitte bestätigen Sie die Kündigung Ihres Abonnements"
msgid "Yes, unsubscribe me from this list"
msgstr "Ja, ich möchte dieses Abonnement kündigen"
msgid ""
"If you received this email by mistake, simply delete it. You won't be "
"unsubscribed if you don't click the confirmation link above."
msgstr ""
"Wenn Sie diese E-Mail versehentlich erhalten haben, löschen Sie sie einfach. "
"Ihr Abonnement wird nicht gekündigt, wenn Sie nicht auf den Bestätigungslink oben klicken."

Binary file not shown.

View file

@ -3027,7 +3027,7 @@ msgstr "Tu dirección email"
#: views/users/account.hbs:9
msgid ""
"This address is used for account recovery in case you loose your password"
"This address is used for account recovery in case you lose your password"
msgstr ""
"Este email es usado para recuperar una cuenta en caso de que no recuerdes la "
"contraseña"
@ -3614,18 +3614,14 @@ msgstr "El segmento especificado no se encontra"
msgid "Selected rule not found"
msgstr "La regla seleccionada no se encontra"
#: lib/models/subscriptions.js:254 lib/models/subscriptions.js:284
msgid "Missing Subbscription ID"
#: lib/models/subscriptions.js:254 lib/models/subscriptions.js:284 lib/models/subscriptions.js:391
msgid "Missing Subscription ID"
msgstr "Falta el ID de Suscripción"
#: lib/models/subscriptions.js:312
msgid "Missing Subbscription email address"
msgid "Missing Subscription email address"
msgstr "Falta el correo electrónico de suscripción"
#: lib/models/subscriptions.js:391
msgid "Missing Subscription ID"
msgstr "Falta el ID de suscripción"
#: lib/models/subscriptions.js:567 lib/models/subscriptions.js:817
msgid "Missing subscription ID"
msgstr "Falta el ID de suscripción"

Binary file not shown.

View file

@ -2300,7 +2300,7 @@ msgstr ""
#: views/users/account.hbs:9
msgid ""
"This address is used for account recovery in case you loose your password"
"This address is used for account recovery in case you lose your password"
msgstr ""
#: views/users/account.hbs:10
@ -2770,11 +2770,11 @@ msgid "Could not save subscription"
msgstr ""
#: lib/models/subscriptions.js:507 lib/models/subscriptions.js:537
msgid "Missing Subbscription ID"
msgid "Missing Subscription ID"
msgstr ""
#: lib/models/subscriptions.js:565
msgid "Missing Subbscription email address"
msgid "Missing Subscription email address"
msgstr ""
#: lib/models/subscriptions.js:644 lib/models/subscriptions.js:893

BIN
languages/it_IT.mo Normal file

Binary file not shown.

4661
languages/it_IT.po Normal file

File diff suppressed because it is too large Load diff

View file

@ -16,7 +16,7 @@ let _ = require('../translate')._;
let util = require('util');
let tableHelpers = require('../table-helpers');
let allowedKeys = ['description', 'from', 'address', 'reply_to', 'subject', 'editor_name', 'editor_data', 'template', 'source_url', 'list', 'segment', 'html', 'text', 'click_tracking_disabled', 'open_tracking_disabled'];
let allowedKeys = ['description', 'from', 'address', 'reply_to', 'subject', 'editor_name', 'editor_data', 'template', 'source_url', 'list', 'segment', 'html', 'text', 'click_tracking_disabled', 'open_tracking_disabled', 'unsubscribe'];
module.exports.list = (start, limit, callback) => {
tableHelpers.list('campaigns', ['*'], 'scheduled', null, start, limit, callback);

View file

@ -11,7 +11,7 @@ const UnsubscriptionMode = require('../../shared/lists').UnsubscriptionMode;
module.exports.UnsubscriptionMode = UnsubscriptionMode;
let allowedKeys = ['description', 'default_form', 'public_subscribe', 'unsubscription_mode'];
let allowedKeys = ['description', 'default_form', 'public_subscribe', 'unsubscription_mode', 'listunsubscribe_disabled'];
module.exports.list = (start, limit, callback) => {
tableHelpers.list('lists', ['*'], 'name', null, start, limit, callback);
@ -150,6 +150,7 @@ module.exports.update = (id, updates, callback) => {
// The update can be only partial when executed from forms/:list
if (!data.customFormChangeOnly) {
data.publicSubscribe = data.publicSubscribe ? 1 : 0;
data.listunsubscribeDisabled = data.listunsubscribeDisabled ? 1 : 0;
data.unsubscriptionMode = Number(data.unsubscriptionMode);
let name = (data.name || '').toString().trim();

View file

@ -156,7 +156,7 @@ module.exports.get = (id, callback) => {
rule.formatted = rule.value.value ? _('Selected') : _('Not selected');
break;
default:
rule.formatted = rule.value.value || '';
rule.formatted = (rule.value.negate ? '!= ' : '') + (rule.value.value || '');
}
return rule;
@ -327,6 +327,7 @@ module.exports.createRule = (segmentId, rule, callback) => {
break;
default:
value = {
negate: Number(rule.negate) ? 1 : 0,
value: rule.value
};
}
@ -418,7 +419,7 @@ module.exports.getRule = (id, callback) => {
rule.formatted = rule.value.value ? _('Selected') : _('Not selected');
break;
default:
rule.formatted = rule.value.value || '';
rule.formatted = (rule.value.negate ? '!= ' : '') + (rule.value.value || '');
}
return callback(null, rule);
@ -491,6 +492,7 @@ module.exports.updateRule = (id, rule, callback) => {
break;
default:
value = {
negate: Number(rule.negate) ? 1 : 0,
value: rule.value
};
}
@ -573,7 +575,8 @@ module.exports.getQuery = (id, prefix, callback) => {
segment.rules.forEach(rule => {
switch (rule.columnType.type) {
case 'string':
query.push(prefix + '`' + rule.columnType.column + '` LIKE ?');
let condition = rule.value.negate ? 'NOT LIKE' : 'LIKE';
query.push(prefix + '`' + rule.columnType.column + '` ' + condition + ' ?');
values.push(rule.value.value);
break;
case 'boolean':

View file

@ -247,7 +247,7 @@ module.exports.get = (listId, cid, callback) => {
cid = (cid || '').toString().trim();
if (!cid) {
return callback(new Error(_('Missing Subbscription ID')));
return callback(new Error(_('Missing Subscription ID')));
}
db.getConnection((err, connection) => {
@ -277,7 +277,7 @@ module.exports.getById = (listId, id, callback) => {
id = Number(id) || 0;
if (!id) {
return callback(new Error(_('Missing Subbscription ID')));
return callback(new Error(_('Missing Subscription ID')));
}
db.getConnection((err, connection) => {
@ -305,7 +305,7 @@ module.exports.getById = (listId, id, callback) => {
module.exports.getByEmail = (listId, email, callback) => {
if (!email) {
return callback(new Error(_('Missing Subbscription email address')));
return callback(new Error(_('Missing Subscription email address')));
}
db.getConnection((err, connection) => {

View file

@ -1,8 +1,10 @@
'use strict';
let log = require('npmlog');
let tools = require('../tools');
let db = require('../db');
let lists = require('./lists');
let segments = require('./segments');
let util = require('util');
let _ = require('../translate')._;
let tableHelpers = require('../table-helpers');
@ -55,7 +57,9 @@ module.exports.list = callback => {
'`triggers`.`description` AS `description`',
'`triggers`.`enabled` AS `enabled`',
'`triggers`.`list` AS `list`',
'`triggers`.`segment` AS `segment`',
'`lists`.`name` AS `list_name`',
'`segments`.`name` AS `segment_name`',
'`source`.`id` AS `source_campaign`',
'`source`.`name` AS `source_campaign_name`',
'`dest`.`id` AS `dest_campaign`',
@ -69,7 +73,7 @@ module.exports.list = callback => {
'`triggers`.`created` AS `created`'
];
let query = 'SELECT ' + tableFields.join(', ') + ' FROM `triggers` LEFT JOIN `campaigns` `source` ON `source`.`id`=`triggers`.`source_campaign` LEFT JOIN `campaigns` `dest` ON `dest`.`id`=`triggers`.`dest_campaign` LEFT JOIN `lists` ON `lists`.`id`=`triggers`.`list` LEFT JOIN `custom_fields` ON `custom_fields`.`list` = `triggers`.`list` AND `custom_fields`.`column`=`triggers`.`column` ORDER BY `triggers`.`name`';
let query = 'SELECT ' + tableFields.join(', ') + ' FROM `triggers` LEFT JOIN `campaigns` `source` ON `source`.`id`=`triggers`.`source_campaign` LEFT JOIN `campaigns` `dest` ON `dest`.`id`=`triggers`.`dest_campaign` LEFT JOIN `lists` ON `lists`.`id`=`triggers`.`list` LEFT JOIN `segments` ON `segments`.`id`=`triggers`.`segment` LEFT JOIN `custom_fields` ON `custom_fields`.`list` = `triggers`.`list` AND `custom_fields`.`column`=`triggers`.`column` ORDER BY `triggers`.`name`';
connection.query(query, (err, rows) => {
connection.release();
if (err) {
@ -105,32 +109,70 @@ module.exports.getQuery = (id, callback) => {
let intervalQuery = (column, seconds, treshold) => column + ' <= NOW() - INTERVAL ' + seconds + ' SECOND AND ' + column + ' >= NOW() - INTERVAL ' + (treshold + seconds) + ' SECOND';
let query = false;
switch (trigger.rule) {
case 'subscription':
query = 'SELECT id FROM `subscription__' + trigger.list + '` subscription WHERE ' + intervalQuery('`' + trigger.column + '`', trigger.seconds, treshold) + ' AND id NOT IN (SELECT subscription FROM `trigger__' + id + '` triggertable WHERE triggertable.`list` = ' + trigger.list + ' AND triggertable.`subscription` = subscription.`id`) LIMIT ' + limit;
break;
case 'campaign':
switch (trigger.column) {
case 'delivered':
query = 'SELECT subscription.id AS id FROM `subscription__' + trigger.list + '` subscription LEFT JOIN `campaign__' + trigger.sourceCampaign + '` campaign ON campaign.list=' + trigger.list + ' AND subscription.id=campaign.subscription WHERE campaign.status=1 AND ' + intervalQuery('`campaign`.`created`', trigger.seconds, treshold) + ' AND subscription.id NOT IN (SELECT subscription FROM `trigger__' + id + '` triggertable WHERE triggertable.`list` = ' + trigger.list + ' AND triggertable.`subscription` = subscription.`id`) LIMIT ' + limit;
break;
case 'not_clicked':
query = 'SELECT subscription.id AS id FROM `subscription__' + trigger.list + '` subscription LEFT JOIN `campaign__' + trigger.sourceCampaign + '` campaign ON campaign.list=' + trigger.list + ' AND subscription.id=campaign.subscription LEFT JOIN `campaign_tracker__' + trigger.sourceCampaign + '` tracker ON tracker.list=campaign.list AND tracker.subscriber=subscription.id AND tracker.link=0 WHERE campaign.status=1 AND ' + intervalQuery('`campaign`.`created`', trigger.seconds, treshold) + ' AND tracker.created IS NULL AND subscription.id NOT IN (SELECT subscription FROM `trigger__' + id + '` triggertable WHERE triggertable.`list` = ' + trigger.list + ' AND triggertable.`subscription` = subscription.`id`) LIMIT ' + limit;
break;
case 'not_opened':
query = 'SELECT subscription.id AS id FROM `subscription__' + trigger.list + '` subscription LEFT JOIN `campaign__' + trigger.sourceCampaign + '` campaign ON campaign.list=' + trigger.list + ' AND subscription.id=campaign.subscription LEFT JOIN `campaign_tracker__' + trigger.sourceCampaign + '` tracker ON tracker.list=campaign.list AND tracker.subscriber=subscription.id AND tracker.link=-1 WHERE campaign.status=1 AND ' + intervalQuery('`campaign`.`created`', trigger.seconds, treshold) + ' AND tracker.created IS NULL AND subscription.id NOT IN (SELECT subscription FROM `trigger__' + id + '` triggertable WHERE triggertable.`list` = ' + trigger.list + ' AND triggertable.`subscription` = subscription.`id`) LIMIT ' + limit;
break;
case 'clicked':
query = 'SELECT subscription.id AS id FROM `subscription__' + trigger.list + '` subscription LEFT JOIN `campaign__' + trigger.sourceCampaign + '` campaign ON campaign.list=' + trigger.list + ' AND subscription.id=campaign.subscription LEFT JOIN `campaign_tracker__' + trigger.sourceCampaign + '` tracker ON tracker.list=campaign.list AND tracker.subscriber=subscription.id AND tracker.link=0 WHERE campaign.status=1 AND ' + intervalQuery('`tracker`.`created`', trigger.seconds, treshold) + ' AND subscription.id NOT IN (SELECT subscription FROM `trigger__' + id + '` triggertable WHERE triggertable.`list` = ' + trigger.list + ' AND triggertable.`subscription` = subscription.`id`) LIMIT ' + limit;
break;
case 'opened':
query = 'SELECT subscription.id AS id FROM `subscription__' + trigger.list + '` subscription LEFT JOIN `campaign__' + trigger.sourceCampaign + '` campaign ON campaign.list=' + trigger.list + ' AND subscription.id=campaign.subscription LEFT JOIN `campaign_tracker__' + trigger.sourceCampaign + '` tracker ON tracker.list=campaign.list AND tracker.subscriber=subscription.id AND tracker.link=-1 WHERE campaign.status=1 AND ' + intervalQuery('`tracker`.`created`', trigger.seconds, treshold) + ' AND subscription.id NOT IN (SELECT subscription FROM `trigger__' + id + '` triggertable WHERE triggertable.`list` = ' + trigger.list + ' AND triggertable.`subscription` = subscription.`id`) LIMIT ' + limit;
break;
let getSegmentQuery = (segmentId, next) => {
segmentId = Number(segmentId);
if (!segmentId) {
return next(null, {
where: '',
values: []
});
}
segments.getQuery(segmentId, 'subscription', next);
};
getSegmentQuery(trigger.segment, (err, queryData) => {
if (err) {
log.err('Triggers', err);
return null;
}
let query = false;
let querySegmentSubscription = '';
let querySegmentTriggertable = '';
if (trigger.segment > 0)
{
querySegmentSubscription = (queryData.where ? ' AND (' + queryData.where + ')' : '');
querySegmentTriggertable = ' AND triggertable.`segment` = ' + trigger.segment;
}
switch (trigger.rule) {
case 'subscription':
query = 'SELECT id FROM `subscription__' + trigger.list + '` subscription WHERE status=1 AND ' + intervalQuery('`' + trigger.column + '`', trigger.seconds, treshold) + ' ' + querySegmentSubscription + ' AND id NOT IN (SELECT subscription FROM `trigger__' + id + '` triggertable WHERE triggertable.`list` = ' + trigger.list + ' ' + querySegmentTriggertable + ' AND triggertable.`subscription` = subscription.`id`) LIMIT ' + limit;
break;
case 'campaign':
switch (trigger.column) {
case 'delivered':
query = 'SELECT subscription.id AS id FROM `subscription__' + trigger.list + '` subscription LEFT JOIN `campaign__' + trigger.sourceCampaign + '` campaign ON campaign.list=' + trigger.list + ' AND subscription.id=campaign.subscription WHERE campaign.status=1 AND ' + intervalQuery('`campaign`.`created`', trigger.seconds, treshold) + ' ' + querySegmentSubscription + ' AND subscription.id NOT IN (SELECT subscription FROM `trigger__' + id + '` triggertable WHERE triggertable.`list` = ' + trigger.list + ' ' + querySegmentTriggertable + ' AND triggertable.`subscription` = subscription.`id`) LIMIT ' + limit;
break;
case 'not_clicked':
query = 'SELECT subscription.id AS id FROM `subscription__' + trigger.list + '` subscription LEFT JOIN `campaign__' + trigger.sourceCampaign + '` campaign ON campaign.list=' + trigger.list + ' AND subscription.id=campaign.subscription LEFT JOIN `campaign_tracker__' + trigger.sourceCampaign + '` tracker ON tracker.list=campaign.list AND tracker.subscriber=subscription.id AND tracker.link=0 WHERE campaign.status=1 AND ' + intervalQuery('`campaign`.`created`', trigger.seconds, treshold) + ' AND tracker.created IS NULL ' + querySegmentSubscription + ' AND subscription.id NOT IN (SELECT subscription FROM `trigger__' + id + '` triggertable WHERE triggertable.`list` = ' + trigger.list + ' ' + querySegmentTriggertable + ' AND triggertable.`subscription` = subscription.`id`) LIMIT ' + limit;
break;
case 'not_opened':
query = 'SELECT subscription.id AS id FROM `subscription__' + trigger.list + '` subscription LEFT JOIN `campaign__' + trigger.sourceCampaign + '` campaign ON campaign.list=' + trigger.list + ' AND subscription.id=campaign.subscription LEFT JOIN `campaign_tracker__' + trigger.sourceCampaign + '` tracker ON tracker.list=campaign.list AND tracker.subscriber=subscription.id AND tracker.link=-1 WHERE campaign.status=1 AND ' + intervalQuery('`campaign`.`created`', trigger.seconds, treshold) + ' AND tracker.created IS NULL ' + querySegmentSubscription + ' AND subscription.id NOT IN (SELECT subscription FROM `trigger__' + id + '` triggertable WHERE triggertable.`list` = ' + trigger.list + ' ' + querySegmentTriggertable + ' AND triggertable.`subscription` = subscription.`id`) LIMIT ' + limit;
break;
case 'clicked':
query = 'SELECT subscription.id AS id FROM `subscription__' + trigger.list + '` subscription LEFT JOIN `campaign__' + trigger.sourceCampaign + '` campaign ON campaign.list=' + trigger.list + ' AND subscription.id=campaign.subscription LEFT JOIN `campaign_tracker__' + trigger.sourceCampaign + '` tracker ON tracker.list=campaign.list AND tracker.subscriber=subscription.id AND tracker.link=0 WHERE campaign.status=1 AND ' + intervalQuery('`tracker`.`created`', trigger.seconds, treshold) + ' ' + querySegmentSubscription + ' AND subscription.id NOT IN (SELECT subscription FROM `trigger__' + id + '` triggertable WHERE triggertable.`list` = ' + trigger.list + ' ' + querySegmentTriggertable + ' AND triggertable.`subscription` = subscription.`id`) LIMIT ' + limit;
break;
case 'opened':
query = 'SELECT subscription.id AS id FROM `subscription__' + trigger.list + '` subscription LEFT JOIN `campaign__' + trigger.sourceCampaign + '` campaign ON campaign.list=' + trigger.list + ' AND subscription.id=campaign.subscription LEFT JOIN `campaign_tracker__' + trigger.sourceCampaign + '` tracker ON tracker.list=campaign.list AND tracker.subscriber=subscription.id AND tracker.link=-1 WHERE campaign.status=1 AND ' + intervalQuery('`tracker`.`created`', trigger.seconds, treshold) + ' ' + querySegmentSubscription + ' AND subscription.id NOT IN (SELECT subscription FROM `trigger__' + id + '` triggertable WHERE triggertable.`list` = ' + trigger.list + ' ' + querySegmentTriggertable + ' AND triggertable.`subscription` = subscription.`id`) LIMIT ' + limit;
break;
}
break;
}
if (trigger.segment > 0) {
let values = queryData.values.concat([trigger.list, trigger.segment]);
for (let i = 0; i < values.length; i++) {
query = query.replace('?', db.escape(values[i]));
}
break;
}
callback(null, query);
}
callback(null, query);
});
});
};
@ -168,6 +210,7 @@ module.exports.create = (trigger, callback) => {
let name = (trigger.name || '').toString().trim();
let description = (trigger.description || '').toString().trim();
let listId = Number(trigger.list) || 0;
let segmentId = Number(trigger.segmentId) || 0;
let seconds = (Number(trigger.days) || 0) * 24 * 3600;
let rule = (trigger.rule || '').toString().toLowerCase().trim();
let destCampaign = Number(trigger.destCampaign) || 0;
@ -220,8 +263,8 @@ module.exports.create = (trigger, callback) => {
return callback(err);
}
let keys = ['name', 'description', 'list', 'source_campaign', 'rule', 'column', 'seconds', 'dest_campaign', 'last_check'];
let values = [name, description, list.id, sourceCampaign, rule, column, seconds, destCampaign];
let keys = ['name', 'description', 'list', 'segment', 'source_campaign', 'rule', 'column', 'seconds', 'dest_campaign', 'last_check'];
let values = [name, description, list.id, segmentId, sourceCampaign, rule, column, seconds, destCampaign];
let query = 'INSERT INTO `triggers` (`' + keys.join('`, `') + '`) VALUES (' + values.map(() => '?').join(', ') + ', NOW())';

File diff suppressed because one or more lines are too long

View file

@ -1,4 +1,5 @@
/* #008f73 #4c9790 */
/* Class names prefixes */
/* Colors / Theme */
.gjs-clm-tags .gjs-sm-title,
.gjs-sm-sector .gjs-sm-title {
border-top: none; }
@ -42,8 +43,7 @@
.gjs-sm-sector .gjs-sm-composite.gjs-clm-field,
.gjs-sm-sector .gjs-sm-field.gjs-sm-composite,
.gjs-sm-sector .gjs-sm-stack #gjs-sm-add {
color: #a0aabf;
/* #a0aabf #d0d6e2 */ }
color: #a0aabf; }
#gjs-rte-toolbar,
.gjs-bg-main,

View file

@ -50,4 +50,4 @@
.cm-s-material .CodeMirror-matchingbracket {
text-decoration: underline;
color: white !important;
}
}

File diff suppressed because one or more lines are too long

View file

@ -85,4 +85,4 @@
-webkit-transform: translate(-0.5rem, 50%);
-ms-transform: translate(-0.5rem, 50%);
transform: translate(-0.5rem, 50%);
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -48,7 +48,7 @@ router.get('/create', passport.csrfProtection, (req, res) => {
data.list = Number(data.list.split(':').shift());
}
getSettings(['defaultFrom', 'defaultAddress', 'defaultSubject'], (err, configItems) => {
settings.list(['defaultFrom', 'defaultAddress', 'defaultSubject', 'defaultUnsubscribe'], (err, configItems) => {
if (err) {
req.flash('danger', err.message || err);
return res.redirect('/');
@ -95,6 +95,7 @@ router.get('/create', passport.csrfProtection, (req, res) => {
data.address = data.address || configItems.defaultAddress;
data.replyTo = data.replyTo || '';
data.subject = data.subject || configItems.defaultSubject;
data.unsubscribe = data.unsubscribe || configItems.defaultUnsubscribe;
let view;
switch (req.query.type) {

View file

@ -19,7 +19,7 @@ const senders = require('../lib/senders');
let settings = require('../lib/models/settings');
let allowedKeys = ['service_url', 'smtp_hostname', 'smtp_port', 'smtp_encryption', 'smtp_disable_auth', 'smtp_user', 'smtp_pass', 'admin_email', 'smtp_log', 'smtp_max_connections', 'smtp_max_messages', 'smtp_self_signed', 'default_from', 'default_address', 'default_subject', 'default_homepage', 'default_postaddress', 'default_sender', 'verp_hostname', 'verp_use', 'disable_wysiwyg', 'pgp_private_key', 'pgp_passphrase', 'ua_code', 'shoutout', 'disable_confirmations', 'smtp_throttling', 'dkim_api_key', 'dkim_private_key', 'dkim_selector', 'dkim_domain', 'mail_transport', 'ses_key', 'ses_secret', 'ses_region'];
let allowedKeys = ['service_url', 'smtp_hostname', 'smtp_port', 'smtp_encryption', 'smtp_disable_auth', 'smtp_user', 'smtp_pass', 'admin_email', 'smtp_log', 'smtp_max_connections', 'smtp_max_messages', 'smtp_self_signed', 'default_from', 'default_address', 'default_subject', 'default_homepage', 'default_postaddress', 'default_sender', 'verp_hostname', 'verp_use', 'disable_wysiwyg', 'pgp_private_key', 'pgp_passphrase', 'ua_code', 'shoutout', 'disable_confirmations', 'smtp_throttling', 'dkim_api_key', 'dkim_private_key', 'dkim_selector', 'dkim_domain', 'mail_transport', 'ses_key', 'ses_secret', 'ses_region', 'x_mailer', 'default_unsubscribe'];
router.all('/*', (req, res, next) => {
if (!req.user) {

View file

@ -5,6 +5,7 @@ let router = new express.Router();
let triggers = require('../lib/models/triggers');
let campaigns = require('../lib/models/campaigns');
let lists = require('../lib/models/lists');
let segments = require('../lib/models/segments');
let fields = require('../lib/models/fields');
let striptags = require('striptags');
let passport = require('../lib/passport');
@ -58,15 +59,23 @@ router.get('/create-select', passport.csrfProtection, (req, res, next) => {
});
router.post('/create-select', passport.parseForm, passport.csrfProtection, (req, res) => {
if (!req.body.list) {
// Check if req.body.list is in correct format ("listId:segmentId")
if (!req.body.list || !/([\d]+):([\d]+)/.test(req.body.list)) {
req.flash('danger', _('Could not find selected list'));
return res.redirect('/triggers/create-select');
}
res.redirect('/triggers/' + encodeURIComponent(req.body.list) + '/create');
let listId = parseInt(req.body.list.split(':')[0]);
let segmentId = parseInt(req.body.list.split(':')[1]);
res.redirect('/triggers/' + encodeURIComponent(listId) + '/' + encodeURIComponent(segmentId) + '/create');
});
router.get('/:listId/create', passport.csrfProtection, (req, res, next) => {
router.get('/:listId/:segmentId/create', passport.csrfProtection, (req, res, next) => {
let listId = parseInt(req.params.listId);
let segmentId = parseInt(req.params.segmentId);
let data = tools.convertKeys(req.query, {
skip: ['layout']
});
@ -74,52 +83,69 @@ router.get('/:listId/create', passport.csrfProtection, (req, res, next) => {
data.csrfToken = req.csrfToken();
data.days = Math.max(Number(data.days) || 1, 1);
lists.get(req.params.listId, (err, list) => {
lists.get(listId, (err, list) => {
if (err || !list) {
req.flash('danger', err && err.message || err || _('Could not find selected list'));
return res.redirect('/triggers/create-select');
}
fields.list(list.id, (err, fieldList) => {
if (err && !fieldList) {
fieldList = [];
segments.get(segmentId, (err, segment) => {
if (segmentId > 0 && err) {
req.flash('danger', err && err.message || err || _('Error while finding selected segment'));
return res.redirect('/triggers/create-select');
}
data.columns = triggers.defaultColumns.concat(fieldList.filter(field => fields.genericTypes[field.type] === 'date')).map(field => ({
column: field.column,
name: field.name,
selected: data.column === field.column
}));
campaigns.list(0, 300, (err, campaignList) => {
if (err) {
return next(err);
segments.subscribers(segmentId, true, (err, segmentSubscribers) => {
if (segmentId > 0 && err) {
req.flash('danger', err && err.message || err || _('Error while finding selected segment'));
return res.redirect('/triggers/create-select');
}
data.sourceCampaigns = (campaignList || []).filter(campaign => campaign.list === list.id).map(campaign => ({
id: campaign.id,
name: campaign.name,
selected: Number(data.sourceCampaign) === campaign.id
}));
fields.list(list.id, (err, fieldList) => {
if (err && !fieldList) {
fieldList = [];
}
data.destCampaigns = (campaignList || []).filter(campaign => campaign.list === list.id && campaign.type === 4).map(campaign => ({
id: campaign.id,
name: campaign.name,
selected: Number(data.destCampaign) === campaign.id
}));
data.columns = triggers.defaultColumns.concat(fieldList.filter(field => fields.genericTypes[field.type] === 'date')).map(field => ({
column: field.column,
name: field.name,
selected: data.column === field.column
}));
data.list = list;
data.isSubscription = data.rule === 'subscription' || !data.rule;
data.isCampaign = data.rule === 'campaign';
campaigns.list(0, 300, (err, campaignList) => {
if (err) {
return next(err);
}
data.campaignOptions = triggers.defaultCampaignEvents.map(evt => ({
option: evt.option,
name: evt.name,
selected: Number(data.sourceCampaign) === evt.option
}));
data.sourceCampaigns = (campaignList || []).filter(campaign => campaign.list === list.id).map(campaign => ({
id: campaign.id,
name: campaign.name,
selected: Number(data.sourceCampaign) === campaign.id
}));
data.isSend = true;
data.destCampaigns = (campaignList || []).filter(campaign => campaign.list === list.id && (segmentId <= 0 || campaign.segment === segmentId) && campaign.type === 4).map(campaign => ({
id: campaign.id,
name: campaign.name,
selected: Number(data.destCampaign) === campaign.id
}));
res.render('triggers/create', data);
data.list = list;
data.segment = segment;
data.segmentSubscribers = segmentSubscribers;
data.isSubscription = data.rule === 'subscription' || !data.rule;
data.isCampaign = data.rule === 'campaign';
data.campaignOptions = triggers.defaultCampaignEvents.map(evt => ({
option: evt.option,
name: evt.name,
selected: Number(data.sourceCampaign) === evt.option
}));
data.isSend = true;
res.render('triggers/create', data);
});
});
});
});
});
@ -154,51 +180,73 @@ router.get('/edit/:id', passport.csrfProtection, (req, res, next) => {
req.flash('danger', err && err.message || err || _('Could not find selected list'));
return res.redirect('/triggers');
}
fields.list(list.id, (err, fieldList) => {
if (err && !fieldList) {
fieldList = [];
segments.get(trigger.segment, (err, segment) => {
if (trigger.segment > 0 && err) {
req.flash('danger', err && err.message || err || _('Error while finding selected segment'));
return res.redirect('/triggers');
}
let segmentId = 0;
if (trigger.segment > 0) {
segmentId = segment.id;
}
campaigns.list(0, 300, (err, campaignList) => {
if (err) {
return next(err);
segments.subscribers(segmentId, true, (err, segmentSubscribers) => {
if (trigger.segment > 0 && err) {
req.flash('danger', err && err.message || err || _('Error while finding selected segment subscribers'));
return res.redirect('/triggers');
}
trigger.sourceCampaigns = (campaignList || []).filter(campaign => campaign.list === list.id).map(campaign => ({
id: campaign.id,
name: campaign.name,
selected: Number(trigger.sourceCampaign) === campaign.id
}));
fields.list(list.id, (err, fieldList) => {
if (err && !fieldList) {
fieldList = [];
}
trigger.destCampaigns = (campaignList || []).filter(campaign => campaign.list === list.id && campaign.type === 4).map(campaign => ({
id: campaign.id,
name: campaign.name,
selected: Number(trigger.destCampaign) === campaign.id
}));
campaigns.list(0, 300, (err, campaignList) => {
if (err) {
return next(err);
}
trigger.list = list;
trigger.isSubscription = trigger.rule === 'subscription' || !trigger.rule;
trigger.isCampaign = trigger.rule === 'campaign';
trigger.sourceCampaigns = (campaignList || []).filter(campaign => campaign.list === list.id).map(campaign => ({
id: campaign.id,
name: campaign.name,
selected: Number(trigger.sourceCampaign) === campaign.id
}));
trigger.columns = triggers.defaultColumns.concat(fieldList.filter(field => fields.genericTypes[field.type] === 'date')).map(field => ({
column: field.column,
name: field.name,
selected: trigger.isSubscription && trigger.column === field.column
}));
trigger.destCampaigns = (campaignList || []).filter(campaign => campaign.list === list.id && campaign.type === 4).map(campaign => ({
id: campaign.id,
name: campaign.name,
selected: Number(trigger.destCampaign) === campaign.id
}));
trigger.campaignOptions = triggers.defaultCampaignEvents.map(evt => ({
option: evt.option,
name: evt.name,
selected: trigger.isCampaign && trigger.column === evt.option
}));
trigger.list = list;
trigger.segment = segment;
trigger.segmentSubscribers = segmentSubscribers;
trigger.isSubscription = trigger.rule === 'subscription' || !trigger.rule;
trigger.isCampaign = trigger.rule === 'campaign';
if (trigger.rule !== 'subscription') {
trigger.column = null;
}
trigger.columns = triggers.defaultColumns.concat(fieldList.filter(field => fields.genericTypes[field.type] === 'date')).map(field => ({
column: field.column,
name: field.name,
selected: trigger.isSubscription && trigger.column === field.column
}));
trigger.isSend = true;
trigger.campaignOptions = triggers.defaultCampaignEvents.map(evt => ({
option: evt.option,
name: evt.name,
selected: trigger.isCampaign && trigger.column === evt.option
}));
res.render('triggers/edit', trigger);
if (trigger.rule !== 'subscription') {
trigger.column = null;
}
trigger.isSend = true;
res.render('triggers/edit', trigger);
});
});
});
});
});

View file

@ -316,7 +316,7 @@ function formatMessage(message, callback) {
return callback(new Error(_('List not found')));
}
getSettings(['serviceUrl', 'verpUse', 'verpHostname'], (err, configItems) => {
settings.list(['serviceUrl', 'verpUse', 'verpHostname', 'xMailer'], (err, configItems) => {
if (err) {
return callback(err);
}
@ -383,13 +383,18 @@ function formatMessage(message, callback) {
wordwrap: 130
});
let listUnsubscribe = null;
if (!list.listunsubscribeDisabled) {
listUnsubscribe = campaign.unsubscribe ? tools.formatMessage(configItems.serviceUrl, campaign, list, message.subscription, campaign.unsubscribe) : url.resolve(configItems.serviceUrl, '/subscription/' + list.cid + '/unsubscribe/' + message.subscription.cid);
}
return callback(null, {
from: {
name: campaign.from,
address: campaign.address
},
replyTo: campaign.replyTo,
xMailer: 'Mailtrain Mailer (+https://mailtrain.org)',
xMailer: configItems.xMailer ? configItems.xMailer : false,
to: {
name: [].concat(message.subscription.firstName || []).concat(message.subscription.lastName || []).join(' '),
address: message.subscription.email
@ -423,7 +428,7 @@ function formatMessage(message, callback) {
}
},
list: {
unsubscribe: url.resolve(configItems.serviceUrl, '/subscription/' + list.cid + '/unsubscribe/' + message.subscription.cid)
unsubscribe: listUnsubscribe
},
subject: tools.formatMessage(configItems.serviceUrl, campaign, list, message.subscription, campaign.subject),
html: renderedHtml,

View file

@ -84,8 +84,8 @@ function fireTrigger(trigger, callback) {
}
let subscriber = rows[pos++].id;
let query = 'INSERT INTO `trigger__' + trigger.id + '` (`list`, `subscription`) VALUES (?,?)';
let values = [trigger.list, subscriber];
let query = 'INSERT INTO `trigger__' + trigger.id + '` (`list`, `segment`, `subscription`) VALUES (?,?,?)';
let values = [trigger.list, trigger.segment, subscriber];
connection.query(query, values, (err, result) => {
if (err && err.code !== 'ER_DUP_ENTRY') {

View file

@ -106,6 +106,12 @@
<input type="text" class="form-control" name="subject" id="subject" value="{{subject}}" placeholder="{{#translate}}Keep it relevant and non-spammy{{/translate}}" required>
</div>
</div>
<div class="form-group">
<label for="unsubscribe" class="col-sm-2 control-label">{{#translate}}Custom unsubscribe (URL){{/translate}}</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="unsubscribe" id="unsubscribe" value="{{unsubscribe}}" placeholder="{{#translate}}Set a custom unsubscribe url{{/translate}}">
</div>
</div>
<div class="col-sm-offset-2">
<div class="checkbox">

View file

@ -117,6 +117,12 @@
<input type="text" class="form-control" name="subject" id="subject" value="{{subject}}" placeholder="{{#translate}}Keep it relevant and non-spammy{{/translate}}" required>
</div>
</div>
<div class="form-group">
<label for="unsubscribe" class="col-sm-2 control-label">{{#translate}}Custom unsubscribe (URL){{/translate}}</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="unsubscribe" id="unsubscribe" value="{{unsubscribe}}" placeholder="{{#translate}}Set a custom unsubscribe url{{/translate}}">
</div>
</div>
<div class="col-sm-offset-2">
<div class="checkbox">

View file

@ -40,18 +40,6 @@
#merge-tag-reference-container table th {
padding: 5px 20px 5px 0;
}
/* Fixed width sidebar */
#gjs > .gjs-editor > .gjs-cv-canvas { width: 100% !important; padding-right: 300px !important; }
#gjs-pn-views, #gjs-pn-views-container { width: 300px !important; }
#gjs-pn-options { right: 300px !important; }
#gjs-pn-commands { width: 100% !important; padding-right: 310px !important; }
/* Hide the fullscreen button - doesn't work from within our iFrame */
#gjs-pn-options .gjs-pn-btn.fa.fa-arrows-alt { display: none !important; }
/* Hide pencil icon on image toolbar, issue #195 */
.gjs-toolbar > div > div.fa-pencil { display: none; }
</style>
@ -68,7 +56,7 @@
<form id="test-form" class="test-form" action="/editorapi/test?editor=grapejs" method="post" style="display: none">
<div class="putsmail-c">
<div class="gjs-sm-property" style="font-size: 10px">
Hello! I'm a placerholder message.
Please enter the email address to send a test to:
<span class="form-status" style="opacity: 0">
<i class="fa fa-refresh anim-spin" aria-hidden="true"></i>
</span>
@ -143,6 +131,12 @@
assets: [],
upload: '/editorapi/upload?type={{type}}&id={{resource.id}}&editor={{editor.name}}',
uploadText: 'Drop images here or click to upload',
headers: {
'X-CSRF-TOKEN': '{{csrfToken}}',
},
},
styleManager: {
clearProperties: true,
},
container : '#gjs',
fromElement: false,
@ -356,13 +350,60 @@
// TODO: Show a spinner
getPreparedHtml(function(html) {
sender.set('active', 0);
var modalContent = md.getContentEl();
var mdlDialog = document.querySelector('.gjs-mdl-dialog');
testContentEl.value = html;
mdlDialog.className += ' ' + mdlClass;
testContainer.style.display = 'block';
md.setTitle('Test your Newsletter');
md.setContent(testContainer);
var modalContent = $('<div>').append(testContainer).html();
for(var i=0; i<100; i++) {
try {
md.setContent(modalContent);
break;
} catch(err) {}
}
testContainerCopy = $(".gjs-mdl-dialog #test-form");
var statusFormElC = document.querySelector('.gjs-mdl-dialog .form-status');
var statusFormEl = document.querySelector('.gjs-mdl-dialog .form-status i');
var ajaxTest = ajaxable(testContainerCopy, { headers: { 'X-CSRF-TOKEN': '{{csrfToken}}' } })
.onStart(function() {
statusFormEl.className = 'fa fa-refresh anim-spin';
statusFormElC.style.opacity = '1';
statusFormElC.className = 'form-status';
})
.onResponse(function(res) {
if (res.data) {
statusFormElC.style.opacity = '0';
statusFormEl.removeAttribute('data-tooltip');
md.close();
toastr.success('Testmail sent');
} else if (res.errors) {
statusFormEl.className = 'fa fa-exclamation-circle';
statusFormEl.setAttribute('data-tooltip', res.errors);
statusFormElC.className = 'form-status text-danger';
toastr.error(res.errors);
}
});
// Remember testemail address
var isValidEmail = function(email) {
return /\S+@\S+\.\S+/.test(email);
};
if (isValidEmail(localStorage.getItem('testemail'))) {
$('.gjs-mdl-dialog #test-form input[name=email]').val(localStorage.getItem('testemail'));
}
$('.gjs-mdl-dialog #test-form').on('submit', function() {
var email = $('.gjs-mdl-dialog #test-form input[name=email]').val();
isValidEmail(email) && localStorage.setItem('testemail', email);
});
md.open();
md.getModel().once('change:open', function() {
mdlDialog.className = mdlDialog.className.replace(mdlClass, '');
@ -382,43 +423,6 @@
},
});
var statusFormElC = document.querySelector('.form-status');
var statusFormEl = document.querySelector('.form-status i');
var ajaxTest = ajaxable(testContainer, { headers: { 'X-CSRF-TOKEN': '{{csrfToken}}' } })
.onStart(function() {
statusFormEl.className = 'fa fa-refresh anim-spin';
statusFormElC.style.opacity = '1';
statusFormElC.className = 'form-status';
})
.onResponse(function(res) {
if (res.data) {
statusFormElC.style.opacity = '0';
statusFormEl.removeAttribute('data-tooltip');
md.close();
toastr.success('Testmail sent');
} else if (res.errors) {
statusFormEl.className = 'fa fa-exclamation-circle';
statusFormEl.setAttribute('data-tooltip', res.errors);
statusFormElC.className = 'form-status text-danger';
toastr.error(res.errors);
}
});
// Remember testemail address
var isValidEmail = function(email) {
return /\S+@\S+\.\S+/.test(email);
};
if (isValidEmail(localStorage.getItem('testemail'))) {
$('#test-form input[name=email]').val(localStorage.getItem('testemail'));
}
$('#test-form').on('submit', function() {
var email = $('#test-form input[name=email]').val();
isValidEmail(email) && localStorage.setItem('testemail', email);
});
// Merge Tag Reference command
@ -431,7 +435,12 @@
mdlDialog.className += ' gjs-mdl-dialog-lg';
mergeTagReferenceContainer.style.display = 'block';
md.setTitle('Merge tag reference');
md.setContent(mergeTagReferenceContainer);
for(var i=0; i<100; i++) {
try {
md.setContent(mergeTagReferenceContainer);
break;
} catch(err) {}
}
md.open();
md.getModel().once('change:open', function() {
mdlDialog.className = mdlDialog.className.replace('gjs-mdl-dialog-lg', '');

View file

@ -4,20 +4,20 @@
<meta charset="utf-8">
<title>GrapesJS Newsletter Editor</title>
<link rel="stylesheet" href="/grapejs/dist/css/grapes.min.css?v=0.5.41">
<link rel="stylesheet" href="/grapejs/dist/css/grapes.min.css?v=0.14.25">
<link rel="stylesheet" href="/grapejs/dist/css/toastr.min.css?v=2.1.3">
<link rel="stylesheet" href="/grapejs/dist/css/material.css">
<link rel="stylesheet" href="/grapejs/dist/css/tooltip.css">
<script src="/javascript/jquery-2.2.1.min.js"></script>
<script src="/grapejs/dist/js/grapes.min.js?v=0.5.41"></script>
<script src="/grapejs/dist/js/grapes.min.js?v=0.14.25"></script>
<script src="/grapejs/dist/js/toastr.min.js?v=2.1.3"></script>
<script src="/grapejs/dist/js/ajaxable.min.js?v=0.2.3"></script>
{{#switch editor.mode}}
{{#case "mjml"}}
<link rel="stylesheet" href="/grapejs/dist/css/grapesjs-mjml.css?v=0.0.7">
<script src="/grapejs/dist/js/grapesjs-mjml.min.js?v=0.0.7"></script>
<script src="/grapejs/dist/js/grapesjs-mjml.min.js?v=0.0.27"></script>
<script src="/grapejs/dist/js/grapesjs-preset-mjml.js"></script>
{{/case}}
{{#case "html"}}

View file

@ -80,6 +80,17 @@
</div>
</div>
<div class="form-group">
<label for="default_form" class="col-sm-2 control-label">{{#translate}}Unsubscribe Header{{/translate}}</label>
<div class="col-sm-10">
<div class="checkbox">
<label>
<input type="checkbox" name="listunsubscribe_disabled" value="1" {{#if listunsubscribeDisabled}} checked {{/if}}> {{#translate}}Do not send List-Unsubscribe headers{{/translate}}
</label>
</div>
</div>
</div>
<hr />
<div class="form-group">

View file

@ -23,6 +23,20 @@
</div>
{{#if columnTypeString}}
<div class="form-group">
<label for="negate" class="col-sm-2 control-label">{{#translate}}Condition{{/translate}}</label>
<div class="col-sm-10">
<select name="negate" id="negate" class="form-control">
<option value="0">
{{#translate}}Equals{{/translate}}
</option>
<option value="1" {{#if value.negate}} selected {{/if}}>
{{#translate}}Not squals{{/translate}}
</option>
</select>
</div>
</div>
<div class="form-group">
<label for="value" class="col-sm-2 control-label">{{#translate}}Value{{/translate}}</label>
<div class="col-sm-10">

View file

@ -28,6 +28,20 @@
</div>
{{#if columnTypeString}}
<div class="form-group">
<label for="negate" class="col-sm-2 control-label">{{#translate}}Condition{{/translate}}</label>
<div class="col-sm-10">
<select name="negate" id="negate" class="form-control">
<option value="0">
{{#translate}}Equals{{/translate}}
</option>
<option value="1" {{#if value.negate}} selected {{/if}}>
{{#translate}}Not squals{{/translate}}
</option>
</select>
</div>
</div>
<div class="form-group">
<label for="value" class="col-sm-2 control-label">{{#translate}}Value{{/translate}}</label>
<div class="col-sm-10">

View file

@ -74,6 +74,14 @@
</div>
</div>
<div class="form-group">
<label for="x-mailer" class="col-sm-2 control-label">{{#translate}}X-Mailer header{{/translate}}</label>
<div class="col-sm-10">
<input type="text" class="form-control" name="x-mailer" id="x-mailer" value="{{xMailer}}">
<span class="help-block">{{#translate}}Set a custom X-Mailer header value or leave empty to disable it{{/translate}}</span>
</div>
</div>
</fieldset>
<fieldset>
@ -122,6 +130,13 @@
<span class="help-block">&nbsp;</span>
</div>
</div>
<div class="form-group">
<label for="default-unsubscribe" class="col-sm-2 control-label">{{#translate}}Default custom unsubscribe (URL)){{/translate}}</label>
<div class="col-sm-10">
<input type="url" class="form-control" name="default-unsubscribe" id="default-unsubscribe" value="{{defaultUnsubscribe}}" placeholder="{{#translate}}Custom unsubscribe URL, eg. http://example.com/unsubscribe/[EMAIL]{{/translate}}">
<span class="help-block">{{#translate}}Set a custom unsubscribe url.{{/translate}}</span>
</div>
</div>
</fieldset>

View file

@ -17,9 +17,19 @@
<select class="form-control" name="list" required>
<option value=""> {{#translate}}Select{{/translate}} </option>
{{#each listItems}}
<option value="{{id}}">
<option value="{{id}}:0">
{{name}} <span class="text-muted"> &mdash; {{subscribers}} {{#translate}}subscribers{{/translate}}</span>
</option>
{{#if segments}}
<optgroup label="{{name}} segments">
{{#each segments}}
<option value="{{../id}}:{{id}}" {{#if selected}} selected {{/if}}>
{{../name}}: {{name}}
</option>
{{/each}}
</optgroup>
{{/if}}
{{/each}}
</select>
</div>

View file

@ -30,7 +30,7 @@
<div class="form-group">
<label class="col-sm-2 control-label">{{#translate}}List{{/translate}}</label>
<div class="col-sm-10">
<p class="form-control-static"><a href="/lists/view/{{list.id}}">{{list.name}}</a> <span class="text-muted"> {{list.subscribers}} {{#translate}}subscribers{{/translate}}</span></p>
<p class="form-control-static"><a href="/lists/view/{{list.id}}">{{list.name}}</a> <span class="text-muted"> {{#if segment.id}} <a href="/lists/view/{{list.id}}?segment={{segment.id}}">{{segment.name}}</a>{{/if}} <span class="text-muted"> {{#if segment.id}} {{segmentSubscribers}} {{else}} {{list.subscribers}} {{/if}} {{#translate}}subscribers{{/translate}}</span><input id="segmentId" name="segmentId" type="hidden" value="{{segment.id}}"></p>
</div>
</div>

View file

@ -43,7 +43,7 @@
<div class="form-group">
<label class="col-sm-2 control-label">{{#translate}}List{{/translate}}</label>
<div class="col-sm-10">
<p class="form-control-static"><a href="/lists/view/{{list.id}}">{{list.name}}</a> <span class="text-muted"> {{list.subscribers}} {{#translate}}subscribers{{/translate}}</span></p>
<p class="form-control-static"><a href="/lists/view/{{list.id}}">{{list.name}}</a> <span class="text-muted"> {{#if segment.id}} <a href="/lists/view/{{list.id}}?segment={{segment.id}}">{{segment.name}}</a>{{/if}} <span class="text-muted"> {{#if segment.id}} {{segmentSubscribers}} {{else}} {{list.subscribers}} {{/if}} {{#translate}}subscribers{{/translate}}</span><input id="segmentId" name="segmentId" type="hidden" value="{{segment.id}}"></p>
</div>
</div>

View file

@ -64,7 +64,7 @@
{{description}}
</td>
<td class="text-info">
<a href="/lists/view/{{list}}">{{listName}}</a>
<a href="/lists/view/{{list}}">{{listName}}</a>{{#if segment}} - <a href="/lists/view/{{list}}?segment={{segment}}">{{segmentName}}</a>{{/if}}
</td>
<td class="text-info">
{{{formatted}}}

View file

@ -1,6 +1,7 @@
{
"name": "mailtrain",
"version": "1.23.2",
"private": true,
"version": "1.24.0",
"description": "Self hosted email newsletter app",
"main": "index.js",
"scripts": {

View file

@ -0,0 +1,17 @@
# Header section
# Define incrementing schema version number
SET @schema_version = '30';
# Upgrade script section
#### INSERT YOUR UPGRADE SCRIPT BELOW THIS LINE ######
ALTER TABLE `lists` ADD COLUMN `listunsubscribe_disabled` tinyint(4) unsigned DEFAULT 0 NOT NULL;
#### INSERT YOUR UPGRADE SCRIPT ABOVE THIS LINE ######
# Footer section. Updates schema version in settings
LOCK TABLES `settings` WRITE;
/*!40000 ALTER TABLE `settings` DISABLE KEYS */;
INSERT INTO `settings` (`key`, `value`) VALUES('db_schema_version', @schema_version) ON DUPLICATE KEY UPDATE `value`=@schema_version;
/*!40000 ALTER TABLE `settings` ENABLE KEYS */;
UNLOCK TABLES;

View file

@ -0,0 +1,12 @@
# Header section
# Define incrementing schema version number
SET @schema_version = '31';
# Add segment support for triggers
ALTER TABLE `triggers` ADD `segment` INT(11) UNSIGNED NOT NULL AFTER `list`;
ALTER TABLE `trigger` ADD `segment` INT(11) UNSIGNED NOT NULL AFTER `list`;
# Footer section
LOCK TABLES `settings` WRITE;
INSERT INTO `settings` (`key`, `value`) VALUES('db_schema_version', @schema_version) ON DUPLICATE KEY UPDATE `value`=@schema_version;
UNLOCK TABLES;

View file

@ -0,0 +1,15 @@
# Header section
# Define incrementing schema version number
SET @schema_version = '32';
# Set default X-Mailer header value
LOCK TABLES `settings` WRITE;
INSERT INTO `settings` (`key`, `value`) VALUES ('x_mailer','Mailtrain Mailer (+https://mailtrain.org)') ON DUPLICATE KEY UPDATE `value`='Mailtrain Mailer (+https://mailtrain.org)';
UNLOCK TABLES;
# Footer section. Updates schema version in settings
LOCK TABLES `settings` WRITE;
/*!40000 ALTER TABLE `settings` DISABLE KEYS */;
INSERT INTO `settings` (`key`, `value`) VALUES('db_schema_version', @schema_version) ON DUPLICATE KEY UPDATE `value`=@schema_version;
/*!40000 ALTER TABLE `settings` ENABLE KEYS */;
UNLOCK TABLES;

View file

@ -0,0 +1,13 @@
# Header section
# Define incrementing schema version number
SET @schema_version = '33';
# Adds new column 'unsubscribe' to campaign table.
ALTER TABLE campaigns ADD COLUMN `unsubscribe` VARCHAR(255) NOT NULL DEFAULT '' AFTER `subject`;
# Footer section. Updates schema version in settings
LOCK TABLES `settings` WRITE;
/*!40000 ALTER TABLE `settings` DISABLE KEYS */;
INSERT INTO `settings` (`key`, `value`) VALUES('db_schema_version', @schema_version) ON DUPLICATE KEY UPDATE `value`=@schema_version;
/*!40000 ALTER TABLE `settings` ENABLE KEYS */;
UNLOCK TABLES;