mirror of
https://github.com/knadh/listmonk.git
synced 2024-09-20 07:16:33 +08:00
Add support for per-campaign custom headers.
- Add new `headers[]` column to the campain table. - Add new headers box to the campaign UI that takes a JSON array of custom headers like the headers on the SMTP settings UI. - Headers are added to e-mails and messenger postback webhooks. - Add cypress tests. Closes #514.
This commit is contained in:
parent
9e9ea0ef15
commit
583dab4bc6
|
@ -292,6 +292,7 @@ func handleCreateCampaign(c echo.Context) error {
|
||||||
o.AltBody,
|
o.AltBody,
|
||||||
o.ContentType,
|
o.ContentType,
|
||||||
o.SendAt,
|
o.SendAt,
|
||||||
|
o.Headers,
|
||||||
pq.StringArray(normalizeTags(o.Tags)),
|
pq.StringArray(normalizeTags(o.Tags)),
|
||||||
o.Messenger,
|
o.Messenger,
|
||||||
o.TemplateID,
|
o.TemplateID,
|
||||||
|
@ -366,6 +367,7 @@ func handleUpdateCampaign(c echo.Context) error {
|
||||||
o.ContentType,
|
o.ContentType,
|
||||||
o.SendAt,
|
o.SendAt,
|
||||||
o.SendLater,
|
o.SendLater,
|
||||||
|
o.Headers,
|
||||||
pq.StringArray(normalizeTags(o.Tags)),
|
pq.StringArray(normalizeTags(o.Tags)),
|
||||||
o.Messenger,
|
o.Messenger,
|
||||||
o.TemplateID,
|
o.TemplateID,
|
||||||
|
@ -603,6 +605,7 @@ func handleTestCampaign(c echo.Context) error {
|
||||||
camp.AltBody = req.AltBody
|
camp.AltBody = req.AltBody
|
||||||
camp.Messenger = req.Messenger
|
camp.Messenger = req.Messenger
|
||||||
camp.ContentType = req.ContentType
|
camp.ContentType = req.ContentType
|
||||||
|
camp.Headers = req.Headers
|
||||||
camp.TemplateID = req.TemplateID
|
camp.TemplateID = req.TemplateID
|
||||||
|
|
||||||
// Send the test messages.
|
// Send the test messages.
|
||||||
|
@ -736,6 +739,10 @@ func validateCampaignFields(c campaignReq, app *App) (campaignReq, error) {
|
||||||
return c, errors.New(app.i18n.Ts("campaigns.fieldInvalidBody", "error", err.Error()))
|
return c, errors.New(app.i18n.Ts("campaigns.fieldInvalidBody", "error", err.Error()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(c.Headers) == 0 {
|
||||||
|
c.Headers = make([]map[string]string, 0)
|
||||||
|
}
|
||||||
|
|
||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
|
@ -146,6 +147,7 @@ func install(lastVer string, db *sqlx.DB, fs stuffbin.FileSystem, prompt, idempo
|
||||||
nil,
|
nil,
|
||||||
"richtext",
|
"richtext",
|
||||||
nil,
|
nil,
|
||||||
|
json.RawMessage("[]"),
|
||||||
pq.StringArray{"test-campaign"},
|
pq.StringArray{"test-campaign"},
|
||||||
emailMsgr,
|
emailMsgr,
|
||||||
1,
|
1,
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
const apiUrl = Cypress.env('apiUrl');
|
const apiUrl = Cypress.env('apiUrl');
|
||||||
|
const headers = '[{"X-Custom": "Custom-Value"}]';
|
||||||
|
|
||||||
describe('Campaigns', () => {
|
describe('Campaigns', () => {
|
||||||
it('Opens campaigns page', () => {
|
it('Opens campaigns page', () => {
|
||||||
|
@ -38,6 +39,10 @@ describe('Campaigns', () => {
|
||||||
cy.wait(100);
|
cy.wait(100);
|
||||||
cy.get('body').click(1, 1);
|
cy.get('body').click(1, 1);
|
||||||
|
|
||||||
|
// Add custom headers.
|
||||||
|
cy.get('[data-cy=btn-headers]').click();
|
||||||
|
cy.get('textarea[name=headers]').invoke('val', headers).trigger('input');
|
||||||
|
|
||||||
// Switch to content tab.
|
// Switch to content tab.
|
||||||
cy.get('.b-tabs nav a').eq(1).click();
|
cy.get('.b-tabs nav a').eq(1).click();
|
||||||
|
|
||||||
|
@ -70,6 +75,7 @@ describe('Campaigns', () => {
|
||||||
expect(data.lists[0].id).to.equal(1);
|
expect(data.lists[0].id).to.equal(1);
|
||||||
expect(data.tags.length).to.equal(1);
|
expect(data.tags.length).to.equal(1);
|
||||||
expect(data.tags[0]).to.equal('new-tag');
|
expect(data.tags[0]).to.equal('new-tag');
|
||||||
|
expect(data.headers[0]['X-Custom']).to.equal('Custom-Value');
|
||||||
});
|
});
|
||||||
|
|
||||||
cy.get('tbody td[data-label=Status] .tag.scheduled');
|
cy.get('tbody td[data-label=Status] .tag.scheduled');
|
||||||
|
@ -181,18 +187,34 @@ describe('Campaigns', () => {
|
||||||
cy.get('input[name=tags]').type(`tag${i}{enter}`);
|
cy.get('input[name=tags]').type(`tag${i}{enter}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add headers.
|
||||||
|
cy.get('[data-cy=btn-headers]').click();
|
||||||
|
cy.get('textarea[name=headers]').invoke('val', `[{"X-Header-${n}": "Value-${n}"}]`).trigger('input');
|
||||||
|
|
||||||
// Hit 'Continue'.
|
// Hit 'Continue'.
|
||||||
cy.get('button[data-cy=btn-continue]').click();
|
cy.get('button[data-cy=btn-continue]').click();
|
||||||
cy.wait(250);
|
cy.wait(250);
|
||||||
|
|
||||||
|
// Verify the changes.
|
||||||
|
(function (n) {
|
||||||
|
cy.location('pathname').then((p) => {
|
||||||
|
cy.request(`${apiUrl}/api/campaigns/${p.split('/').at(-1)}`).should((response) => {
|
||||||
|
const { data } = response.body;
|
||||||
|
expect(data.status).to.equal('draft');
|
||||||
|
expect(data.name).to.equal(`name${n}`);
|
||||||
|
expect(data.subject).to.equal(`subject${n}`);
|
||||||
|
expect(data.content_type).to.equal('richtext');
|
||||||
|
expect(data.altbody).to.equal(null);
|
||||||
|
expect(data.send_at).to.equal(null);
|
||||||
|
expect(data.headers[0][`X-Header-${n}`]).to.equal(`Value-${n}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})(n);
|
||||||
|
|
||||||
|
|
||||||
// Select content type.
|
// Select content type.
|
||||||
cy.get(`label[data-cy=check-${c}]`).click();
|
cy.get(`label[data-cy=check-${c}]`).click();
|
||||||
|
|
||||||
// If it's not richtext, there's a "you'll lose formatting" prompt.
|
|
||||||
if (c !== 'richtext') {
|
|
||||||
cy.get('.modal button.is-primary').click();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert content.
|
// Insert content.
|
||||||
const htmlBody = `<strong>hello${n}</strong> \{\{ .Subscriber.Name \}\} from {\{ .Subscriber.Attribs.city \}\}`;
|
const htmlBody = `<strong>hello${n}</strong> \{\{ .Subscriber.Name \}\} from {\{ .Subscriber.Attribs.city \}\}`;
|
||||||
const plainBody = `hello${n} Demo Subscriber from Bengaluru`;
|
const plainBody = `hello${n} Demo Subscriber from Bengaluru`;
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
<b-tag v-if="data.type === 'optin'" :class="data.type">
|
<b-tag v-if="data.type === 'optin'" :class="data.type">
|
||||||
{{ $t('lists.optin') }}
|
{{ $t('lists.optin') }}
|
||||||
</b-tag>
|
</b-tag>
|
||||||
<span v-if="isEditing" class="has-text-grey-light is-size-7">
|
<span v-if="isEditing" class="has-text-grey-light is-size-7" :data-campaign-id="data.id">
|
||||||
{{ $t('globals.fields.id') }}: {{ data.id }} /
|
{{ $t('globals.fields.id') }}: {{ data.id }} /
|
||||||
{{ $t('globals.fields.uuid') }}: {{ data.uuid }}
|
{{ $t('globals.fields.uuid') }}: {{ data.uuid }}
|
||||||
</span>
|
</span>
|
||||||
|
@ -22,7 +22,7 @@
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<b-field grouped v-if="isEditing && canEdit">
|
<b-field grouped v-if="isEditing && canEdit">
|
||||||
<b-field expanded>
|
<b-field expanded>
|
||||||
<b-button expanded @click="onSubmit" :loading="loading.campaigns"
|
<b-button expanded @click="() => onSubmit('update')" :loading="loading.campaigns"
|
||||||
type="is-primary" icon-left="content-save-outline" data-cy="btn-save">
|
type="is-primary" icon-left="content-save-outline" data-cy="btn-save">
|
||||||
{{ $t('globals.buttons.saveChanges') }}
|
{{ $t('globals.buttons.saveChanges') }}
|
||||||
</b-button>
|
</b-button>
|
||||||
|
@ -53,7 +53,7 @@
|
||||||
<section class="wrap">
|
<section class="wrap">
|
||||||
<div class="columns">
|
<div class="columns">
|
||||||
<div class="column is-7">
|
<div class="column is-7">
|
||||||
<form @submit.prevent="onSubmit">
|
<form @submit.prevent="() => onSubmit(isNew ? 'create' : 'update')">
|
||||||
<b-field :label="$t('globals.fields.name')" label-position="on-border">
|
<b-field :label="$t('globals.fields.name')" label-position="on-border">
|
||||||
<b-input :maxlength="200" :ref="'focus'" v-model="form.name"
|
<b-input :maxlength="200" :ref="'focus'" v-model="form.name"
|
||||||
name="name" :disabled="!canEdit"
|
name="name" :disabled="!canEdit"
|
||||||
|
@ -124,6 +124,20 @@
|
||||||
</b-field>
|
</b-field>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p class="has-text-right">
|
||||||
|
<a href="#" class="is-size-7" @click.prevent="showHeaders"
|
||||||
|
data-cy="btn-headers">
|
||||||
|
<b-icon icon="plus" />{{ $t('settings.smtp.setCustomHeaders') }}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<b-field v-if="form.headersStr !== '[]' || isHeadersVisible"
|
||||||
|
label-position="on-border" :message="$t('campaigns.customHeadersHelp')">
|
||||||
|
<b-input v-model="form.headersStr" name="headers" type="textarea"
|
||||||
|
placeholder='[{"X-Custom": "value"}, {"X-Custom2": "value"}]' />
|
||||||
|
</b-field>
|
||||||
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<b-field v-if="isNew">
|
<b-field v-if="isNew">
|
||||||
|
@ -140,12 +154,12 @@
|
||||||
<h3 class="title is-size-6">{{ $t('campaigns.sendTest') }}</h3>
|
<h3 class="title is-size-6">{{ $t('campaigns.sendTest') }}</h3>
|
||||||
<b-field :message="$t('campaigns.sendTestHelp')">
|
<b-field :message="$t('campaigns.sendTestHelp')">
|
||||||
<b-taginput v-model="form.testEmails"
|
<b-taginput v-model="form.testEmails"
|
||||||
:before-adding="$utils.validateEmail" :disabled="this.isNew"
|
:before-adding="$utils.validateEmail" :disabled="isNew"
|
||||||
ellipsis icon="email-outline" :placeholder="$t('campaigns.testEmails')" />
|
ellipsis icon="email-outline" :placeholder="$t('campaigns.testEmails')" />
|
||||||
</b-field>
|
</b-field>
|
||||||
<b-field>
|
<b-field>
|
||||||
<b-button @click="sendTest" :loading="loading.campaigns" :disabled="this.isNew"
|
<b-button @click="() => onSubmit('test')" :loading="loading.campaigns"
|
||||||
type="is-primary" icon-left="email-outline">
|
:disabled="isNew" type="is-primary" icon-left="email-outline">
|
||||||
{{ $t('campaigns.send') }}
|
{{ $t('campaigns.send') }}
|
||||||
</b-button>
|
</b-button>
|
||||||
</b-field>
|
</b-field>
|
||||||
|
@ -204,6 +218,7 @@ export default Vue.extend({
|
||||||
return {
|
return {
|
||||||
isNew: false,
|
isNew: false,
|
||||||
isEditing: false,
|
isEditing: false,
|
||||||
|
isHeadersVisible: false,
|
||||||
activeTab: 0,
|
activeTab: 0,
|
||||||
|
|
||||||
data: {},
|
data: {},
|
||||||
|
@ -216,6 +231,8 @@ export default Vue.extend({
|
||||||
name: '',
|
name: '',
|
||||||
subject: '',
|
subject: '',
|
||||||
fromEmail: '',
|
fromEmail: '',
|
||||||
|
headersStr: '[]',
|
||||||
|
headers: [],
|
||||||
templateId: 0,
|
templateId: 0,
|
||||||
lists: [],
|
lists: [],
|
||||||
tags: [],
|
tags: [],
|
||||||
|
@ -245,11 +262,32 @@ export default Vue.extend({
|
||||||
this.form.altbody = null;
|
this.form.altbody = null;
|
||||||
},
|
},
|
||||||
|
|
||||||
onSubmit() {
|
showHeaders() {
|
||||||
if (this.isNew) {
|
this.isHeadersVisible = !this.isHeadersVisible;
|
||||||
this.createCampaign();
|
},
|
||||||
|
|
||||||
|
onSubmit(typ) {
|
||||||
|
if (this.form.headersStr && this.form.headersStr !== '[]') {
|
||||||
|
try {
|
||||||
|
this.form.headers = JSON.parse(this.form.headersStr);
|
||||||
|
} catch (e) {
|
||||||
|
this.$utils.toast(e.toString(), 'is-danger');
|
||||||
|
return;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
this.updateCampaign();
|
this.form.headers = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (typ) {
|
||||||
|
case 'create':
|
||||||
|
this.createCampaign();
|
||||||
|
break;
|
||||||
|
case 'test':
|
||||||
|
this.sendTest();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this.updateCampaign();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -259,6 +297,7 @@ export default Vue.extend({
|
||||||
this.form = {
|
this.form = {
|
||||||
...this.form,
|
...this.form,
|
||||||
...data,
|
...data,
|
||||||
|
headersStr: JSON.stringify(data.headers, null, 4),
|
||||||
|
|
||||||
// The structure that is populated by editor input event.
|
// The structure that is populated by editor input event.
|
||||||
content: { contentType: data.contentType, body: data.body },
|
content: { contentType: data.contentType, body: data.body },
|
||||||
|
@ -280,6 +319,7 @@ export default Vue.extend({
|
||||||
from_email: this.form.fromEmail,
|
from_email: this.form.fromEmail,
|
||||||
messenger: this.form.messenger,
|
messenger: this.form.messenger,
|
||||||
type: 'regular',
|
type: 'regular',
|
||||||
|
headers: this.form.headers,
|
||||||
tags: this.form.tags,
|
tags: this.form.tags,
|
||||||
template_id: this.form.templateId,
|
template_id: this.form.templateId,
|
||||||
content_type: this.form.content.contentType,
|
content_type: this.form.content.contentType,
|
||||||
|
@ -306,6 +346,7 @@ export default Vue.extend({
|
||||||
tags: this.form.tags,
|
tags: this.form.tags,
|
||||||
send_later: this.form.sendLater,
|
send_later: this.form.sendLater,
|
||||||
send_at: this.form.sendLater ? this.form.sendAtDate : null,
|
send_at: this.form.sendLater ? this.form.sendAtDate : null,
|
||||||
|
headers: this.form.headers,
|
||||||
template_id: this.form.templateId,
|
template_id: this.form.templateId,
|
||||||
// body: this.form.body,
|
// body: this.form.body,
|
||||||
};
|
};
|
||||||
|
@ -327,6 +368,7 @@ export default Vue.extend({
|
||||||
tags: this.form.tags,
|
tags: this.form.tags,
|
||||||
send_later: this.form.sendLater,
|
send_later: this.form.sendLater,
|
||||||
send_at: this.form.sendLater ? this.form.sendAtDate : null,
|
send_at: this.form.sendLater ? this.form.sendAtDate : null,
|
||||||
|
headers: this.form.headers,
|
||||||
template_id: this.form.templateId,
|
template_id: this.form.templateId,
|
||||||
content_type: this.form.content.contentType,
|
content_type: this.form.content.contentType,
|
||||||
body: this.form.content.body,
|
body: this.form.content.body,
|
||||||
|
|
|
@ -142,7 +142,7 @@
|
||||||
<b-icon icon="plus" />{{ $t('settings.smtp.setCustomHeaders') }}</a>
|
<b-icon icon="plus" />{{ $t('settings.smtp.setCustomHeaders') }}</a>
|
||||||
</p>
|
</p>
|
||||||
<b-field v-if="item.email_headers.length > 0 || item.showHeaders"
|
<b-field v-if="item.email_headers.length > 0 || item.showHeaders"
|
||||||
:label="$t('')" label-position="on-border"
|
label-position="on-border"
|
||||||
:message="$t('settings.smtp.customHeadersHelp')">
|
:message="$t('settings.smtp.customHeadersHelp')">
|
||||||
<b-input v-model="item.strEmailHeaders" name="email_headers" type="textarea"
|
<b-input v-model="item.strEmailHeaders" name="email_headers" type="textarea"
|
||||||
placeholder='[{"X-Custom": "value"}, {"X-Custom2": "value"}]' />
|
placeholder='[{"X-Custom": "value"}, {"X-Custom2": "value"}]' />
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
"campaigns.contentHelp": "Obsah zde",
|
"campaigns.contentHelp": "Obsah zde",
|
||||||
"campaigns.continue": "Pokračovat",
|
"campaigns.continue": "Pokračovat",
|
||||||
"campaigns.copyOf": "Kopie {name}",
|
"campaigns.copyOf": "Kopie {name}",
|
||||||
|
"campaigns.customHeadersHelp": "Array of custom headers to attach to outgoing messages. eg: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
|
||||||
"campaigns.dateAndTime": "Datum a čas",
|
"campaigns.dateAndTime": "Datum a čas",
|
||||||
"campaigns.ended": "Ukončeno",
|
"campaigns.ended": "Ukončeno",
|
||||||
"campaigns.errorSendTest": "Chyba při odesílání testu: {error}",
|
"campaigns.errorSendTest": "Chyba při odesílání testu: {error}",
|
||||||
|
@ -35,6 +36,7 @@
|
||||||
"campaigns.fromAddress": "Z adresy",
|
"campaigns.fromAddress": "Z adresy",
|
||||||
"campaigns.fromAddressPlaceholder": "Vaše jméno <noreply@yoursite.com>",
|
"campaigns.fromAddressPlaceholder": "Vaše jméno <noreply@yoursite.com>",
|
||||||
"campaigns.invalid": "Neplatná kampaň",
|
"campaigns.invalid": "Neplatná kampaň",
|
||||||
|
"campaigns.invalidCustomHeaders": "Invalid custom headers: {error}",
|
||||||
"campaigns.markdown": "Sleva",
|
"campaigns.markdown": "Sleva",
|
||||||
"campaigns.needsSendAt": "Kampaň musí mít naplánované datum.",
|
"campaigns.needsSendAt": "Kampaň musí mít naplánované datum.",
|
||||||
"campaigns.newCampaign": "Nová kampaň",
|
"campaigns.newCampaign": "Nová kampaň",
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
"campaigns.contentHelp": "Inhalt hier",
|
"campaigns.contentHelp": "Inhalt hier",
|
||||||
"campaigns.continue": "Fortsetzen",
|
"campaigns.continue": "Fortsetzen",
|
||||||
"campaigns.copyOf": "Kopie von {name}",
|
"campaigns.copyOf": "Kopie von {name}",
|
||||||
|
"campaigns.customHeadersHelp": "Array of custom headers to attach to outgoing messages. eg: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
|
||||||
"campaigns.dateAndTime": "Datum und Zeit",
|
"campaigns.dateAndTime": "Datum und Zeit",
|
||||||
"campaigns.ended": "Abgeschlossen",
|
"campaigns.ended": "Abgeschlossen",
|
||||||
"campaigns.errorSendTest": "Fehler beim Senden der Testmail: {error}",
|
"campaigns.errorSendTest": "Fehler beim Senden der Testmail: {error}",
|
||||||
|
@ -35,6 +36,7 @@
|
||||||
"campaigns.fromAddress": "Absender",
|
"campaigns.fromAddress": "Absender",
|
||||||
"campaigns.fromAddressPlaceholder": "Dein Name <noreply@deineseite.de>",
|
"campaigns.fromAddressPlaceholder": "Dein Name <noreply@deineseite.de>",
|
||||||
"campaigns.invalid": "Ungültige Kampagne",
|
"campaigns.invalid": "Ungültige Kampagne",
|
||||||
|
"campaigns.invalidCustomHeaders": "Invalid custom headers: {error}",
|
||||||
"campaigns.markdown": "Markdown",
|
"campaigns.markdown": "Markdown",
|
||||||
"campaigns.needsSendAt": "Die Kampagne benötigt ein `send_at` Sendedatum, um automatisch verschickt zu werden.",
|
"campaigns.needsSendAt": "Die Kampagne benötigt ein `send_at` Sendedatum, um automatisch verschickt zu werden.",
|
||||||
"campaigns.newCampaign": "Neue Kampagne",
|
"campaigns.newCampaign": "Neue Kampagne",
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
"campaigns.contentHelp": "Content here",
|
"campaigns.contentHelp": "Content here",
|
||||||
"campaigns.continue": "Continue",
|
"campaigns.continue": "Continue",
|
||||||
"campaigns.copyOf": "Copy of {name}",
|
"campaigns.copyOf": "Copy of {name}",
|
||||||
|
"campaigns.customHeadersHelp": "Array of custom headers to attach to outgoing messages. eg: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
|
||||||
"campaigns.dateAndTime": "Date and time",
|
"campaigns.dateAndTime": "Date and time",
|
||||||
"campaigns.ended": "Ended",
|
"campaigns.ended": "Ended",
|
||||||
"campaigns.errorSendTest": "Error sending test: {error}",
|
"campaigns.errorSendTest": "Error sending test: {error}",
|
||||||
|
@ -35,6 +36,7 @@
|
||||||
"campaigns.fromAddress": "From address",
|
"campaigns.fromAddress": "From address",
|
||||||
"campaigns.fromAddressPlaceholder": "Your Name <noreply@yoursite.com>",
|
"campaigns.fromAddressPlaceholder": "Your Name <noreply@yoursite.com>",
|
||||||
"campaigns.invalid": "Invalid campaign",
|
"campaigns.invalid": "Invalid campaign",
|
||||||
|
"campaigns.invalidCustomHeaders": "Invalid custom headers: {error}",
|
||||||
"campaigns.markdown": "Markdown",
|
"campaigns.markdown": "Markdown",
|
||||||
"campaigns.needsSendAt": "Campaign needs a date to be scheduled.",
|
"campaigns.needsSendAt": "Campaign needs a date to be scheduled.",
|
||||||
"campaigns.newCampaign": "New campaign",
|
"campaigns.newCampaign": "New campaign",
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
"campaigns.contentHelp": "Contenido aqui",
|
"campaigns.contentHelp": "Contenido aqui",
|
||||||
"campaigns.continue": "Continuar",
|
"campaigns.continue": "Continuar",
|
||||||
"campaigns.copyOf": "Copia de {name}",
|
"campaigns.copyOf": "Copia de {name}",
|
||||||
|
"campaigns.customHeadersHelp": "Array of custom headers to attach to outgoing messages. eg: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
|
||||||
"campaigns.dateAndTime": "Fecha y hora",
|
"campaigns.dateAndTime": "Fecha y hora",
|
||||||
"campaigns.ended": "Finalizado",
|
"campaigns.ended": "Finalizado",
|
||||||
"campaigns.errorSendTest": "Error al enviar la prueba: {error}",
|
"campaigns.errorSendTest": "Error al enviar la prueba: {error}",
|
||||||
|
@ -35,6 +36,7 @@
|
||||||
"campaigns.fromAddress": "Dirección origen",
|
"campaigns.fromAddress": "Dirección origen",
|
||||||
"campaigns.fromAddressPlaceholder": "Su Nombre <noresponder@susitio.com>",
|
"campaigns.fromAddressPlaceholder": "Su Nombre <noresponder@susitio.com>",
|
||||||
"campaigns.invalid": "Campaña no válida",
|
"campaigns.invalid": "Campaña no válida",
|
||||||
|
"campaigns.invalidCustomHeaders": "Invalid custom headers: {error}",
|
||||||
"campaigns.markdown": "Markdown",
|
"campaigns.markdown": "Markdown",
|
||||||
"campaigns.needsSendAt": "Una campaña necesita una fecha pra ser agendada.",
|
"campaigns.needsSendAt": "Una campaña necesita una fecha pra ser agendada.",
|
||||||
"campaigns.newCampaign": "Nueva campaña",
|
"campaigns.newCampaign": "Nueva campaña",
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
"campaigns.contentHelp": "Rédigez le contenu ici.",
|
"campaigns.contentHelp": "Rédigez le contenu ici.",
|
||||||
"campaigns.continue": "Continuer",
|
"campaigns.continue": "Continuer",
|
||||||
"campaigns.copyOf": "Copie de {name}",
|
"campaigns.copyOf": "Copie de {name}",
|
||||||
|
"campaigns.customHeadersHelp": "Array of custom headers to attach to outgoing messages. eg: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
|
||||||
"campaigns.dateAndTime": "Date et heure",
|
"campaigns.dateAndTime": "Date et heure",
|
||||||
"campaigns.ended": "Terminée",
|
"campaigns.ended": "Terminée",
|
||||||
"campaigns.errorSendTest": "Erreur lors de l'envoi du test : {error}",
|
"campaigns.errorSendTest": "Erreur lors de l'envoi du test : {error}",
|
||||||
|
@ -35,6 +36,7 @@
|
||||||
"campaigns.fromAddress": "Adresse d'envoi",
|
"campaigns.fromAddress": "Adresse d'envoi",
|
||||||
"campaigns.fromAddressPlaceholder": "Nom à afficher <noreply@votresite.com>",
|
"campaigns.fromAddressPlaceholder": "Nom à afficher <noreply@votresite.com>",
|
||||||
"campaigns.invalid": "Campagne non valide",
|
"campaigns.invalid": "Campagne non valide",
|
||||||
|
"campaigns.invalidCustomHeaders": "Invalid custom headers: {error}",
|
||||||
"campaigns.markdown": "Markdown",
|
"campaigns.markdown": "Markdown",
|
||||||
"campaigns.needsSendAt": "Une date est nécessaire pour planifier la campagne.",
|
"campaigns.needsSendAt": "Une date est nécessaire pour planifier la campagne.",
|
||||||
"campaigns.newCampaign": "Nouvelle campagne",
|
"campaigns.newCampaign": "Nouvelle campagne",
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
"campaigns.contentHelp": "Tartalom itt",
|
"campaigns.contentHelp": "Tartalom itt",
|
||||||
"campaigns.continue": "Folytatás",
|
"campaigns.continue": "Folytatás",
|
||||||
"campaigns.copyOf": "Másolata a {name}",
|
"campaigns.copyOf": "Másolata a {name}",
|
||||||
|
"campaigns.customHeadersHelp": "Array of custom headers to attach to outgoing messages. eg: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
|
||||||
"campaigns.dateAndTime": "Dátum és Idő",
|
"campaigns.dateAndTime": "Dátum és Idő",
|
||||||
"campaigns.ended": "Befejezett",
|
"campaigns.ended": "Befejezett",
|
||||||
"campaigns.errorSendTest": "Hiba a teszt küldésekor: {error}",
|
"campaigns.errorSendTest": "Hiba a teszt küldésekor: {error}",
|
||||||
|
@ -35,6 +36,7 @@
|
||||||
"campaigns.fromAddress": "Címről",
|
"campaigns.fromAddress": "Címről",
|
||||||
"campaigns.fromAddressPlaceholder": "A neved <noreply@yoursite.com>",
|
"campaigns.fromAddressPlaceholder": "A neved <noreply@yoursite.com>",
|
||||||
"campaigns.invalid": "Érvénytelen kampány",
|
"campaigns.invalid": "Érvénytelen kampány",
|
||||||
|
"campaigns.invalidCustomHeaders": "Invalid custom headers: {error}",
|
||||||
"campaigns.markdown": "Csökkentés",
|
"campaigns.markdown": "Csökkentés",
|
||||||
"campaigns.needsSendAt": "A kampányhoz dátumot kell beállítani.",
|
"campaigns.needsSendAt": "A kampányhoz dátumot kell beállítani.",
|
||||||
"campaigns.newCampaign": "Új kampány",
|
"campaigns.newCampaign": "Új kampány",
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
"campaigns.contentHelp": "Contenuto qui",
|
"campaigns.contentHelp": "Contenuto qui",
|
||||||
"campaigns.continue": "Continuare",
|
"campaigns.continue": "Continuare",
|
||||||
"campaigns.copyOf": "Copie di {name}",
|
"campaigns.copyOf": "Copie di {name}",
|
||||||
|
"campaigns.customHeadersHelp": "Array of custom headers to attach to outgoing messages. eg: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
|
||||||
"campaigns.dateAndTime": "Data e ora",
|
"campaigns.dateAndTime": "Data e ora",
|
||||||
"campaigns.ended": "Finito",
|
"campaigns.ended": "Finito",
|
||||||
"campaigns.errorSendTest": "Errore durante il test di invio: {error}",
|
"campaigns.errorSendTest": "Errore durante il test di invio: {error}",
|
||||||
|
@ -35,6 +36,7 @@
|
||||||
"campaigns.fromAddress": "Mittente",
|
"campaigns.fromAddress": "Mittente",
|
||||||
"campaigns.fromAddressPlaceholder": "Tuo nome <noreply@tuosito.com>",
|
"campaigns.fromAddressPlaceholder": "Tuo nome <noreply@tuosito.com>",
|
||||||
"campaigns.invalid": "Campagna non valida",
|
"campaigns.invalid": "Campagna non valida",
|
||||||
|
"campaigns.invalidCustomHeaders": "Invalid custom headers: {error}",
|
||||||
"campaigns.markdown": "Markdown",
|
"campaigns.markdown": "Markdown",
|
||||||
"campaigns.needsSendAt": "È necessaria una data per programmare la campagna.",
|
"campaigns.needsSendAt": "È necessaria una data per programmare la campagna.",
|
||||||
"campaigns.newCampaign": "Nuova campagna",
|
"campaigns.newCampaign": "Nuova campagna",
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
"campaigns.contentHelp": "ഇവിടെ ഉള്ളടക്കം നൽകുക",
|
"campaigns.contentHelp": "ഇവിടെ ഉള്ളടക്കം നൽകുക",
|
||||||
"campaigns.continue": "തുടരൂ",
|
"campaigns.continue": "തുടരൂ",
|
||||||
"campaigns.copyOf": "{name} ന്റെ പകർപ്പ്",
|
"campaigns.copyOf": "{name} ന്റെ പകർപ്പ്",
|
||||||
|
"campaigns.customHeadersHelp": "Array of custom headers to attach to outgoing messages. eg: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
|
||||||
"campaigns.dateAndTime": "തിയതിയും സമയവും",
|
"campaigns.dateAndTime": "തിയതിയും സമയവും",
|
||||||
"campaigns.ended": "അവസാനിച്ചു",
|
"campaigns.ended": "അവസാനിച്ചു",
|
||||||
"campaigns.errorSendTest": "ടെസ്റ്റ് അയയ്ക്കുന്നത് പരാജയപ്പെട്ടു: {error}",
|
"campaigns.errorSendTest": "ടെസ്റ്റ് അയയ്ക്കുന്നത് പരാജയപ്പെട്ടു: {error}",
|
||||||
|
@ -35,6 +36,7 @@
|
||||||
"campaigns.fromAddress": "പ്രേക്ഷകൻ",
|
"campaigns.fromAddress": "പ്രേക്ഷകൻ",
|
||||||
"campaigns.fromAddressPlaceholder": "നിങ്ങളുടെ പേര് <noreply@yoursite.com>",
|
"campaigns.fromAddressPlaceholder": "നിങ്ങളുടെ പേര് <noreply@yoursite.com>",
|
||||||
"campaigns.invalid": "ക്യാമ്പേയ്ൻ അസാധുവാണ്",
|
"campaigns.invalid": "ക്യാമ്പേയ്ൻ അസാധുവാണ്",
|
||||||
|
"campaigns.invalidCustomHeaders": "Invalid custom headers: {error}",
|
||||||
"campaigns.markdown": "Markdown",
|
"campaigns.markdown": "Markdown",
|
||||||
"campaigns.needsSendAt": "ക്യാമ്പേയ്ന് `send_at` തിയതി മുൻകൂട്ടി നിശ്ചയിക്കേണ്ടതുണ്ട്.",
|
"campaigns.needsSendAt": "ക്യാമ്പേയ്ന് `send_at` തിയതി മുൻകൂട്ടി നിശ്ചയിക്കേണ്ടതുണ്ട്.",
|
||||||
"campaigns.newCampaign": "പുതിയ ക്യാമ്പേയ്ൻ",
|
"campaigns.newCampaign": "പുതിയ ക്യാമ്പേയ്ൻ",
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
"campaigns.contentHelp": "Inhoud hier",
|
"campaigns.contentHelp": "Inhoud hier",
|
||||||
"campaigns.continue": "Hervatten",
|
"campaigns.continue": "Hervatten",
|
||||||
"campaigns.copyOf": "Kopie van {name}",
|
"campaigns.copyOf": "Kopie van {name}",
|
||||||
|
"campaigns.customHeadersHelp": "Array of custom headers to attach to outgoing messages. eg: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
|
||||||
"campaigns.dateAndTime": "Datum en tijd",
|
"campaigns.dateAndTime": "Datum en tijd",
|
||||||
"campaigns.ended": "Beëindigd",
|
"campaigns.ended": "Beëindigd",
|
||||||
"campaigns.errorSendTest": "Fout bij verzenden test: {error}",
|
"campaigns.errorSendTest": "Fout bij verzenden test: {error}",
|
||||||
|
@ -35,6 +36,7 @@
|
||||||
"campaigns.fromAddress": "Afzender",
|
"campaigns.fromAddress": "Afzender",
|
||||||
"campaigns.fromAddressPlaceholder": "Jouw Naam <noreply@yoursite.com>",
|
"campaigns.fromAddressPlaceholder": "Jouw Naam <noreply@yoursite.com>",
|
||||||
"campaigns.invalid": "Ongeldige campagne",
|
"campaigns.invalid": "Ongeldige campagne",
|
||||||
|
"campaigns.invalidCustomHeaders": "Invalid custom headers: {error}",
|
||||||
"campaigns.markdown": "Markdown",
|
"campaigns.markdown": "Markdown",
|
||||||
"campaigns.needsSendAt": "Campagne heeft een datum nodig om ingepland te worden.",
|
"campaigns.needsSendAt": "Campagne heeft een datum nodig om ingepland te worden.",
|
||||||
"campaigns.newCampaign": "Nieuwe campagne",
|
"campaigns.newCampaign": "Nieuwe campagne",
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
"campaigns.contentHelp": "Zgoda tutaj",
|
"campaigns.contentHelp": "Zgoda tutaj",
|
||||||
"campaigns.continue": "Kontynuuj",
|
"campaigns.continue": "Kontynuuj",
|
||||||
"campaigns.copyOf": "Kopia {name}",
|
"campaigns.copyOf": "Kopia {name}",
|
||||||
|
"campaigns.customHeadersHelp": "Array of custom headers to attach to outgoing messages. eg: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
|
||||||
"campaigns.dateAndTime": "Data i czas",
|
"campaigns.dateAndTime": "Data i czas",
|
||||||
"campaigns.ended": "Zakończona",
|
"campaigns.ended": "Zakończona",
|
||||||
"campaigns.errorSendTest": "Błąd wysyłania testu: {error}",
|
"campaigns.errorSendTest": "Błąd wysyłania testu: {error}",
|
||||||
|
@ -35,6 +36,7 @@
|
||||||
"campaigns.fromAddress": "Adres od",
|
"campaigns.fromAddress": "Adres od",
|
||||||
"campaigns.fromAddressPlaceholder": "Twoja Nazwa <noreply@yoursite.com>",
|
"campaigns.fromAddressPlaceholder": "Twoja Nazwa <noreply@yoursite.com>",
|
||||||
"campaigns.invalid": "Nieprawidłowa kampania",
|
"campaigns.invalid": "Nieprawidłowa kampania",
|
||||||
|
"campaigns.invalidCustomHeaders": "Invalid custom headers: {error}",
|
||||||
"campaigns.markdown": "Markdown",
|
"campaigns.markdown": "Markdown",
|
||||||
"campaigns.needsSendAt": "Kampania wymaga daty w celu zaplanowania.",
|
"campaigns.needsSendAt": "Kampania wymaga daty w celu zaplanowania.",
|
||||||
"campaigns.newCampaign": "Nowa kampania",
|
"campaigns.newCampaign": "Nowa kampania",
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
"campaigns.contentHelp": "Conteúdo aqui",
|
"campaigns.contentHelp": "Conteúdo aqui",
|
||||||
"campaigns.continue": "Continuar",
|
"campaigns.continue": "Continuar",
|
||||||
"campaigns.copyOf": "Cópia de {name}",
|
"campaigns.copyOf": "Cópia de {name}",
|
||||||
|
"campaigns.customHeadersHelp": "Array of custom headers to attach to outgoing messages. eg: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
|
||||||
"campaigns.dateAndTime": "Data e hora",
|
"campaigns.dateAndTime": "Data e hora",
|
||||||
"campaigns.ended": "Finalizada",
|
"campaigns.ended": "Finalizada",
|
||||||
"campaigns.errorSendTest": "Erro ao enviar o teste: {error}",
|
"campaigns.errorSendTest": "Erro ao enviar o teste: {error}",
|
||||||
|
@ -35,6 +36,7 @@
|
||||||
"campaigns.fromAddress": "Endereço do remetente",
|
"campaigns.fromAddress": "Endereço do remetente",
|
||||||
"campaigns.fromAddressPlaceholder": "Seu Nome <noreply@yoursite.com>",
|
"campaigns.fromAddressPlaceholder": "Seu Nome <noreply@yoursite.com>",
|
||||||
"campaigns.invalid": "Campanha inválida",
|
"campaigns.invalid": "Campanha inválida",
|
||||||
|
"campaigns.invalidCustomHeaders": "Invalid custom headers: {error}",
|
||||||
"campaigns.markdown": "Markdown",
|
"campaigns.markdown": "Markdown",
|
||||||
"campaigns.needsSendAt": "A campanha precisa de uma data para ser programada.",
|
"campaigns.needsSendAt": "A campanha precisa de uma data para ser programada.",
|
||||||
"campaigns.newCampaign": "Nova campanha",
|
"campaigns.newCampaign": "Nova campanha",
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
"campaigns.contentHelp": "Conteúdo aqui",
|
"campaigns.contentHelp": "Conteúdo aqui",
|
||||||
"campaigns.continue": "Continuar",
|
"campaigns.continue": "Continuar",
|
||||||
"campaigns.copyOf": "Cópia de {name}",
|
"campaigns.copyOf": "Cópia de {name}",
|
||||||
|
"campaigns.customHeadersHelp": "Array of custom headers to attach to outgoing messages. eg: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
|
||||||
"campaigns.dateAndTime": "Dia e hora",
|
"campaigns.dateAndTime": "Dia e hora",
|
||||||
"campaigns.ended": "Terminada",
|
"campaigns.ended": "Terminada",
|
||||||
"campaigns.errorSendTest": "Erro ao enviar teste: {error}",
|
"campaigns.errorSendTest": "Erro ao enviar teste: {error}",
|
||||||
|
@ -35,6 +36,7 @@
|
||||||
"campaigns.fromAddress": "Endereço do Remetente",
|
"campaigns.fromAddress": "Endereço do Remetente",
|
||||||
"campaigns.fromAddressPlaceholder": "O Teu Nome <noreply@oteusite.com>",
|
"campaigns.fromAddressPlaceholder": "O Teu Nome <noreply@oteusite.com>",
|
||||||
"campaigns.invalid": "Campanha inválida",
|
"campaigns.invalid": "Campanha inválida",
|
||||||
|
"campaigns.invalidCustomHeaders": "Invalid custom headers: {error}",
|
||||||
"campaigns.markdown": "Markdown",
|
"campaigns.markdown": "Markdown",
|
||||||
"campaigns.needsSendAt": "A campanha necessita de uma data para ser agendada.",
|
"campaigns.needsSendAt": "A campanha necessita de uma data para ser agendada.",
|
||||||
"campaigns.newCampaign": "Nova campanha",
|
"campaigns.newCampaign": "Nova campanha",
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
"campaigns.contentHelp": "Conținut aici",
|
"campaigns.contentHelp": "Conținut aici",
|
||||||
"campaigns.continue": "Continuă",
|
"campaigns.continue": "Continuă",
|
||||||
"campaigns.copyOf": "Copie a {nume}",
|
"campaigns.copyOf": "Copie a {nume}",
|
||||||
|
"campaigns.customHeadersHelp": "Array of custom headers to attach to outgoing messages. eg: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
|
||||||
"campaigns.dateAndTime": "Dată și oră",
|
"campaigns.dateAndTime": "Dată și oră",
|
||||||
"campaigns.ended": "Terminat",
|
"campaigns.ended": "Terminat",
|
||||||
"campaigns.errorSendTest": "Eroare trimitere test: {erore}",
|
"campaigns.errorSendTest": "Eroare trimitere test: {erore}",
|
||||||
|
@ -35,6 +36,7 @@
|
||||||
"campaigns.fromAddress": "De la adresa",
|
"campaigns.fromAddress": "De la adresa",
|
||||||
"campaigns.fromAddressPlaceholder": "Numele tau <noreply@yoursite.com>",
|
"campaigns.fromAddressPlaceholder": "Numele tau <noreply@yoursite.com>",
|
||||||
"campaigns.invalid": "Campanie nevalidă",
|
"campaigns.invalid": "Campanie nevalidă",
|
||||||
|
"campaigns.invalidCustomHeaders": "Invalid custom headers: {error}",
|
||||||
"campaigns.markdown": "Markdown",
|
"campaigns.markdown": "Markdown",
|
||||||
"campaigns.needsSendAt": "Campania are nevoie de o dată pentru a fi programată",
|
"campaigns.needsSendAt": "Campania are nevoie de o dată pentru a fi programată",
|
||||||
"campaigns.newCampaign": "Campanie nouă",
|
"campaigns.newCampaign": "Campanie nouă",
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
"campaigns.contentHelp": "Содержимое",
|
"campaigns.contentHelp": "Содержимое",
|
||||||
"campaigns.continue": "Продолжить",
|
"campaigns.continue": "Продолжить",
|
||||||
"campaigns.copyOf": "Копия {name}",
|
"campaigns.copyOf": "Копия {name}",
|
||||||
|
"campaigns.customHeadersHelp": "Array of custom headers to attach to outgoing messages. eg: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
|
||||||
"campaigns.dateAndTime": "Дата и время",
|
"campaigns.dateAndTime": "Дата и время",
|
||||||
"campaigns.ended": "Окончено",
|
"campaigns.ended": "Окончено",
|
||||||
"campaigns.errorSendTest": "Ошибка отправки теста: {error}",
|
"campaigns.errorSendTest": "Ошибка отправки теста: {error}",
|
||||||
|
@ -35,6 +36,7 @@
|
||||||
"campaigns.fromAddress": "Адрес отправителя",
|
"campaigns.fromAddress": "Адрес отправителя",
|
||||||
"campaigns.fromAddressPlaceholder": "Ваше имя <noreply@yoursite.com>",
|
"campaigns.fromAddressPlaceholder": "Ваше имя <noreply@yoursite.com>",
|
||||||
"campaigns.invalid": "Неверная компания",
|
"campaigns.invalid": "Неверная компания",
|
||||||
|
"campaigns.invalidCustomHeaders": "Invalid custom headers: {error}",
|
||||||
"campaigns.markdown": "Разметка",
|
"campaigns.markdown": "Разметка",
|
||||||
"campaigns.needsSendAt": "Для планирования компании необходима дата.",
|
"campaigns.needsSendAt": "Для планирования компании необходима дата.",
|
||||||
"campaigns.newCampaign": "Новая компания",
|
"campaigns.newCampaign": "Новая компания",
|
||||||
|
|
|
@ -21,6 +21,7 @@
|
||||||
"campaigns.contentHelp": "İçerik buraya",
|
"campaigns.contentHelp": "İçerik buraya",
|
||||||
"campaigns.continue": "Devam et",
|
"campaigns.continue": "Devam et",
|
||||||
"campaigns.copyOf": "{name} - Kopyası",
|
"campaigns.copyOf": "{name} - Kopyası",
|
||||||
|
"campaigns.customHeadersHelp": "Array of custom headers to attach to outgoing messages. eg: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
|
||||||
"campaigns.dateAndTime": "Tarih ve saat",
|
"campaigns.dateAndTime": "Tarih ve saat",
|
||||||
"campaigns.ended": "Bitti",
|
"campaigns.ended": "Bitti",
|
||||||
"campaigns.errorSendTest": "Test gönderirken hata: {error}",
|
"campaigns.errorSendTest": "Test gönderirken hata: {error}",
|
||||||
|
@ -35,6 +36,7 @@
|
||||||
"campaigns.fromAddress": "Gelen adres",
|
"campaigns.fromAddress": "Gelen adres",
|
||||||
"campaigns.fromAddressPlaceholder": "isminiz <cevap-verme@siteniz.com>",
|
"campaigns.fromAddressPlaceholder": "isminiz <cevap-verme@siteniz.com>",
|
||||||
"campaigns.invalid": "Yanlış tanımlı kapmanya",
|
"campaigns.invalid": "Yanlış tanımlı kapmanya",
|
||||||
|
"campaigns.invalidCustomHeaders": "Invalid custom headers: {error}",
|
||||||
"campaigns.markdown": "Markdown",
|
"campaigns.markdown": "Markdown",
|
||||||
"campaigns.needsSendAt": "Kampanya için tanımlanmış bir tarih gerekli.",
|
"campaigns.needsSendAt": "Kampanya için tanımlanmış bir tarih gerekli.",
|
||||||
"campaigns.newCampaign": "Yeni kampanya",
|
"campaigns.newCampaign": "Yeni kampanya",
|
||||||
|
|
|
@ -319,6 +319,15 @@ func (m *Manager) worker() {
|
||||||
h.Set("List-Unsubscribe", `<`+msg.unsubURL+`>`)
|
h.Set("List-Unsubscribe", `<`+msg.unsubURL+`>`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Attach any custom headers.
|
||||||
|
if len(msg.Campaign.Headers) > 0 {
|
||||||
|
for _, set := range msg.Campaign.Headers {
|
||||||
|
for hdr, val := range set {
|
||||||
|
h.Add(hdr, val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
out.Headers = h
|
out.Headers = h
|
||||||
|
|
||||||
if err := m.messengers[msg.Campaign.Messenger].Push(out); err != nil {
|
if err := m.messengers[msg.Campaign.Messenger].Push(out); err != nil {
|
||||||
|
|
|
@ -24,9 +24,10 @@ type postback struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type campaign struct {
|
type campaign struct {
|
||||||
UUID string `db:"uuid" json:"uuid"`
|
UUID string `db:"uuid" json:"uuid"`
|
||||||
Name string `db:"name" json:"name"`
|
Name string `db:"name" json:"name"`
|
||||||
Tags []string `db:"tags" json:"tags"`
|
Headers models.Headers `db:"headers" json:"headers"`
|
||||||
|
Tags []string `db:"tags" json:"tags"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type recipient struct {
|
type recipient struct {
|
||||||
|
@ -100,9 +101,10 @@ func (p *Postback) Push(m messenger.Message) error {
|
||||||
|
|
||||||
if m.Campaign != nil {
|
if m.Campaign != nil {
|
||||||
pb.Campaign = &campaign{
|
pb.Campaign = &campaign{
|
||||||
UUID: m.Campaign.UUID,
|
UUID: m.Campaign.UUID,
|
||||||
Name: m.Campaign.Name,
|
Name: m.Campaign.Name,
|
||||||
Tags: m.Campaign.Tags,
|
Headers: m.Campaign.Headers,
|
||||||
|
Tags: m.Campaign.Tags,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -179,6 +179,43 @@ func easyjsonDf11841fDecodeGithubComKnadhListmonkInternalMessengerPostback2(in *
|
||||||
out.UUID = string(in.String())
|
out.UUID = string(in.String())
|
||||||
case "name":
|
case "name":
|
||||||
out.Name = string(in.String())
|
out.Name = string(in.String())
|
||||||
|
case "headers":
|
||||||
|
if in.IsNull() {
|
||||||
|
in.Skip()
|
||||||
|
out.Headers = nil
|
||||||
|
} else {
|
||||||
|
in.Delim('[')
|
||||||
|
if out.Headers == nil {
|
||||||
|
if !in.IsDelim(']') {
|
||||||
|
out.Headers = make(models.Headers, 0, 8)
|
||||||
|
} else {
|
||||||
|
out.Headers = models.Headers{}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
out.Headers = (out.Headers)[:0]
|
||||||
|
}
|
||||||
|
for !in.IsDelim(']') {
|
||||||
|
var v4 map[string]string
|
||||||
|
if in.IsNull() {
|
||||||
|
in.Skip()
|
||||||
|
} else {
|
||||||
|
in.Delim('{')
|
||||||
|
v4 = make(map[string]string)
|
||||||
|
for !in.IsDelim('}') {
|
||||||
|
key := string(in.String())
|
||||||
|
in.WantColon()
|
||||||
|
var v5 string
|
||||||
|
v5 = string(in.String())
|
||||||
|
(v4)[key] = v5
|
||||||
|
in.WantComma()
|
||||||
|
}
|
||||||
|
in.Delim('}')
|
||||||
|
}
|
||||||
|
out.Headers = append(out.Headers, v4)
|
||||||
|
in.WantComma()
|
||||||
|
}
|
||||||
|
in.Delim(']')
|
||||||
|
}
|
||||||
case "tags":
|
case "tags":
|
||||||
if in.IsNull() {
|
if in.IsNull() {
|
||||||
in.Skip()
|
in.Skip()
|
||||||
|
@ -195,9 +232,9 @@ func easyjsonDf11841fDecodeGithubComKnadhListmonkInternalMessengerPostback2(in *
|
||||||
out.Tags = (out.Tags)[:0]
|
out.Tags = (out.Tags)[:0]
|
||||||
}
|
}
|
||||||
for !in.IsDelim(']') {
|
for !in.IsDelim(']') {
|
||||||
var v4 string
|
var v6 string
|
||||||
v4 = string(in.String())
|
v6 = string(in.String())
|
||||||
out.Tags = append(out.Tags, v4)
|
out.Tags = append(out.Tags, v6)
|
||||||
in.WantComma()
|
in.WantComma()
|
||||||
}
|
}
|
||||||
in.Delim(']')
|
in.Delim(']')
|
||||||
|
@ -226,6 +263,38 @@ func easyjsonDf11841fEncodeGithubComKnadhListmonkInternalMessengerPostback2(out
|
||||||
out.RawString(prefix)
|
out.RawString(prefix)
|
||||||
out.String(string(in.Name))
|
out.String(string(in.Name))
|
||||||
}
|
}
|
||||||
|
{
|
||||||
|
const prefix string = ",\"headers\":"
|
||||||
|
out.RawString(prefix)
|
||||||
|
if in.Headers == nil && (out.Flags&jwriter.NilSliceAsEmpty) == 0 {
|
||||||
|
out.RawString("null")
|
||||||
|
} else {
|
||||||
|
out.RawByte('[')
|
||||||
|
for v7, v8 := range in.Headers {
|
||||||
|
if v7 > 0 {
|
||||||
|
out.RawByte(',')
|
||||||
|
}
|
||||||
|
if v8 == nil && (out.Flags&jwriter.NilMapAsEmpty) == 0 {
|
||||||
|
out.RawString(`null`)
|
||||||
|
} else {
|
||||||
|
out.RawByte('{')
|
||||||
|
v9First := true
|
||||||
|
for v9Name, v9Value := range v8 {
|
||||||
|
if v9First {
|
||||||
|
v9First = false
|
||||||
|
} else {
|
||||||
|
out.RawByte(',')
|
||||||
|
}
|
||||||
|
out.String(string(v9Name))
|
||||||
|
out.RawByte(':')
|
||||||
|
out.String(string(v9Value))
|
||||||
|
}
|
||||||
|
out.RawByte('}')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.RawByte(']')
|
||||||
|
}
|
||||||
|
}
|
||||||
{
|
{
|
||||||
const prefix string = ",\"tags\":"
|
const prefix string = ",\"tags\":"
|
||||||
out.RawString(prefix)
|
out.RawString(prefix)
|
||||||
|
@ -233,11 +302,11 @@ func easyjsonDf11841fEncodeGithubComKnadhListmonkInternalMessengerPostback2(out
|
||||||
out.RawString("null")
|
out.RawString("null")
|
||||||
} else {
|
} else {
|
||||||
out.RawByte('[')
|
out.RawByte('[')
|
||||||
for v5, v6 := range in.Tags {
|
for v10, v11 := range in.Tags {
|
||||||
if v5 > 0 {
|
if v10 > 0 {
|
||||||
out.RawByte(',')
|
out.RawByte(',')
|
||||||
}
|
}
|
||||||
out.String(string(v6))
|
out.String(string(v11))
|
||||||
}
|
}
|
||||||
out.RawByte(']')
|
out.RawByte(']')
|
||||||
}
|
}
|
||||||
|
@ -278,15 +347,15 @@ func easyjsonDf11841fDecodeGithubComKnadhListmonkInternalMessengerPostback1(in *
|
||||||
for !in.IsDelim('}') {
|
for !in.IsDelim('}') {
|
||||||
key := string(in.String())
|
key := string(in.String())
|
||||||
in.WantColon()
|
in.WantColon()
|
||||||
var v7 interface{}
|
var v12 interface{}
|
||||||
if m, ok := v7.(easyjson.Unmarshaler); ok {
|
if m, ok := v12.(easyjson.Unmarshaler); ok {
|
||||||
m.UnmarshalEasyJSON(in)
|
m.UnmarshalEasyJSON(in)
|
||||||
} else if m, ok := v7.(json.Unmarshaler); ok {
|
} else if m, ok := v12.(json.Unmarshaler); ok {
|
||||||
_ = m.UnmarshalJSON(in.Raw())
|
_ = m.UnmarshalJSON(in.Raw())
|
||||||
} else {
|
} else {
|
||||||
v7 = in.Interface()
|
v12 = in.Interface()
|
||||||
}
|
}
|
||||||
(out.Attribs)[key] = v7
|
(out.Attribs)[key] = v12
|
||||||
in.WantComma()
|
in.WantComma()
|
||||||
}
|
}
|
||||||
in.Delim('}')
|
in.Delim('}')
|
||||||
|
@ -329,21 +398,21 @@ func easyjsonDf11841fEncodeGithubComKnadhListmonkInternalMessengerPostback1(out
|
||||||
out.RawString(`null`)
|
out.RawString(`null`)
|
||||||
} else {
|
} else {
|
||||||
out.RawByte('{')
|
out.RawByte('{')
|
||||||
v8First := true
|
v13First := true
|
||||||
for v8Name, v8Value := range in.Attribs {
|
for v13Name, v13Value := range in.Attribs {
|
||||||
if v8First {
|
if v13First {
|
||||||
v8First = false
|
v13First = false
|
||||||
} else {
|
} else {
|
||||||
out.RawByte(',')
|
out.RawByte(',')
|
||||||
}
|
}
|
||||||
out.String(string(v8Name))
|
out.String(string(v13Name))
|
||||||
out.RawByte(':')
|
out.RawByte(':')
|
||||||
if m, ok := v8Value.(easyjson.Marshaler); ok {
|
if m, ok := v13Value.(easyjson.Marshaler); ok {
|
||||||
m.MarshalEasyJSON(out)
|
m.MarshalEasyJSON(out)
|
||||||
} else if m, ok := v8Value.(json.Marshaler); ok {
|
} else if m, ok := v13Value.(json.Marshaler); ok {
|
||||||
out.Raw(m.MarshalJSON())
|
out.Raw(m.MarshalJSON())
|
||||||
} else {
|
} else {
|
||||||
out.Raw(json.Marshal(v8Value))
|
out.Raw(json.Marshal(v13Value))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
out.RawByte('}')
|
out.RawByte('}')
|
||||||
|
|
|
@ -8,7 +8,7 @@ import (
|
||||||
|
|
||||||
// V2_1_0 performs the DB migrations for v.2.1.0.
|
// V2_1_0 performs the DB migrations for v.2.1.0.
|
||||||
func V2_1_0(db *sqlx.DB, fs stuffbin.FileSystem, ko *koanf.Koanf) error {
|
func V2_1_0(db *sqlx.DB, fs stuffbin.FileSystem, ko *koanf.Koanf) error {
|
||||||
// Insert into appearance related settings.
|
// Insert appearance related settings.
|
||||||
if _, err := db.Exec(`
|
if _, err := db.Exec(`
|
||||||
INSERT INTO settings (key, value) VALUES
|
INSERT INTO settings (key, value) VALUES
|
||||||
('appearance.admin.custom_css', '""'),
|
('appearance.admin.custom_css', '""'),
|
||||||
|
@ -34,5 +34,9 @@ func V2_1_0(db *sqlx.DB, fs stuffbin.FileSystem, ko *koanf.Koanf) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if _, err := db.Exec(`ALTER TABLE campaigns ADD COLUMN IF NOT EXISTS headers JSONB NOT NULL DEFAULT '[]';`); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -72,6 +72,10 @@ const (
|
||||||
BounceTypeSoft = "soft"
|
BounceTypeSoft = "soft"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Headers represents an array of string maps used to represent SMTP, HTTP headers etc.
|
||||||
|
// similar to url.Values{}
|
||||||
|
type Headers []map[string]string
|
||||||
|
|
||||||
// regTplFunc represents contains a regular expression for wrapping and
|
// regTplFunc represents contains a regular expression for wrapping and
|
||||||
// substituting a Go template function from the user's shorthand to a full
|
// substituting a Go template function from the user's shorthand to a full
|
||||||
// function call.
|
// function call.
|
||||||
|
@ -193,6 +197,7 @@ type Campaign struct {
|
||||||
Status string `db:"status" json:"status"`
|
Status string `db:"status" json:"status"`
|
||||||
ContentType string `db:"content_type" json:"content_type"`
|
ContentType string `db:"content_type" json:"content_type"`
|
||||||
Tags pq.StringArray `db:"tags" json:"tags"`
|
Tags pq.StringArray `db:"tags" json:"tags"`
|
||||||
|
Headers Headers `db:"headers" json:"headers"`
|
||||||
TemplateID int `db:"template_id" json:"template_id"`
|
TemplateID int `db:"template_id" json:"template_id"`
|
||||||
Messenger string `db:"messenger" json:"messenger"`
|
Messenger string `db:"messenger" json:"messenger"`
|
||||||
|
|
||||||
|
@ -468,3 +473,40 @@ func (s Subscriber) LastName() string {
|
||||||
|
|
||||||
return s.Name
|
return s.Name
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Scan implements the sql.Scanner interface.
|
||||||
|
func (h *Headers) Scan(src interface{}) error {
|
||||||
|
var b []byte
|
||||||
|
switch src := src.(type) {
|
||||||
|
case []byte:
|
||||||
|
b = src
|
||||||
|
case string:
|
||||||
|
b = []byte(src)
|
||||||
|
case nil:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(b, h); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value implements the driver.Valuer interface.
|
||||||
|
func (h Headers) Value() (driver.Value, error) {
|
||||||
|
if h == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if n := len(h); n > 0 {
|
||||||
|
b, err := json.Marshal(h)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return "[]", nil
|
||||||
|
}
|
||||||
|
|
27
queries.sql
27
queries.sql
|
@ -388,16 +388,16 @@ WITH campLists AS (
|
||||||
-- Get the list_ids and their optin statuses for the campaigns found in the previous step.
|
-- Get the list_ids and their optin statuses for the campaigns found in the previous step.
|
||||||
SELECT lists.id AS list_id, campaign_id, optin FROM lists
|
SELECT lists.id AS list_id, campaign_id, optin FROM lists
|
||||||
INNER JOIN campaign_lists ON (campaign_lists.list_id = lists.id)
|
INNER JOIN campaign_lists ON (campaign_lists.list_id = lists.id)
|
||||||
WHERE lists.id = ANY($13::INT[])
|
WHERE lists.id = ANY($14::INT[])
|
||||||
),
|
),
|
||||||
tpl AS (
|
tpl AS (
|
||||||
-- If there's no template_id given, use the defualt template.
|
-- If there's no template_id given, use the defualt template.
|
||||||
SELECT (CASE WHEN $12 = 0 THEN id ELSE $12 END) AS id FROM templates WHERE is_default IS TRUE
|
SELECT (CASE WHEN $13 = 0 THEN id ELSE $13 END) AS id FROM templates WHERE is_default IS TRUE
|
||||||
),
|
),
|
||||||
counts AS (
|
counts AS (
|
||||||
SELECT COALESCE(COUNT(id), 0) as to_send, COALESCE(MAX(id), 0) as max_sub_id
|
SELECT COALESCE(COUNT(id), 0) as to_send, COALESCE(MAX(id), 0) as max_sub_id
|
||||||
FROM subscribers
|
FROM subscribers
|
||||||
LEFT JOIN campLists ON (campLists.campaign_id = ANY($13::INT[]))
|
LEFT JOIN campLists ON (campLists.campaign_id = ANY($14::INT[]))
|
||||||
LEFT JOIN subscriber_lists ON (
|
LEFT JOIN subscriber_lists ON (
|
||||||
subscriber_lists.status != 'unsubscribed' AND
|
subscriber_lists.status != 'unsubscribed' AND
|
||||||
subscribers.id = subscriber_lists.subscriber_id AND
|
subscribers.id = subscriber_lists.subscriber_id AND
|
||||||
|
@ -407,16 +407,16 @@ counts AS (
|
||||||
-- any status except for 'unsubscribed' (already excluded above) works.
|
-- any status except for 'unsubscribed' (already excluded above) works.
|
||||||
(CASE WHEN campLists.optin = 'double' THEN subscriber_lists.status = 'confirmed' ELSE true END)
|
(CASE WHEN campLists.optin = 'double' THEN subscriber_lists.status = 'confirmed' ELSE true END)
|
||||||
)
|
)
|
||||||
WHERE subscriber_lists.list_id=ANY($13::INT[])
|
WHERE subscriber_lists.list_id=ANY($14::INT[])
|
||||||
AND subscribers.status='enabled'
|
AND subscribers.status='enabled'
|
||||||
),
|
),
|
||||||
camp AS (
|
camp AS (
|
||||||
INSERT INTO campaigns (uuid, type, name, subject, from_email, body, altbody, content_type, send_at, tags, messenger, template_id, to_send, max_subscriber_id)
|
INSERT INTO campaigns (uuid, type, name, subject, from_email, body, altbody, content_type, send_at, headers, tags, messenger, template_id, to_send, max_subscriber_id)
|
||||||
SELECT $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, (SELECT id FROM tpl), (SELECT to_send FROM counts), (SELECT max_sub_id FROM counts)
|
SELECT $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, (SELECT id FROM tpl), (SELECT to_send FROM counts), (SELECT max_sub_id FROM counts)
|
||||||
RETURNING id
|
RETURNING id
|
||||||
)
|
)
|
||||||
INSERT INTO campaign_lists (campaign_id, list_id, list_name)
|
INSERT INTO campaign_lists (campaign_id, list_id, list_name)
|
||||||
(SELECT (SELECT id FROM camp), id, name FROM lists WHERE id=ANY($13::INT[]))
|
(SELECT (SELECT id FROM camp), id, name FROM lists WHERE id=ANY($14::INT[]))
|
||||||
RETURNING (SELECT id FROM camp);
|
RETURNING (SELECT id FROM camp);
|
||||||
|
|
||||||
-- name: query-campaigns
|
-- name: query-campaigns
|
||||||
|
@ -428,7 +428,7 @@ INSERT INTO campaign_lists (campaign_id, list_id, list_name)
|
||||||
-- with every resultant row.
|
-- with every resultant row.
|
||||||
SELECT c.id, c.uuid, c.name, c.subject, c.from_email,
|
SELECT c.id, c.uuid, c.name, c.subject, c.from_email,
|
||||||
c.messenger, c.started_at, c.to_send, c.sent, c.type,
|
c.messenger, c.started_at, c.to_send, c.sent, c.type,
|
||||||
c.body, c.altbody, c.send_at, c.status, c.content_type, c.tags,
|
c.body, c.altbody, c.send_at, c.headers, c.status, c.content_type, c.tags,
|
||||||
c.template_id, c.created_at, c.updated_at,
|
c.template_id, c.created_at, c.updated_at,
|
||||||
COUNT(*) OVER () AS total,
|
COUNT(*) OVER () AS total,
|
||||||
(
|
(
|
||||||
|
@ -666,18 +666,19 @@ WITH camp AS (
|
||||||
content_type=$7::content_type,
|
content_type=$7::content_type,
|
||||||
send_at=$8::TIMESTAMP WITH TIME ZONE,
|
send_at=$8::TIMESTAMP WITH TIME ZONE,
|
||||||
status=(CASE WHEN NOT $9 THEN 'draft' ELSE status END),
|
status=(CASE WHEN NOT $9 THEN 'draft' ELSE status END),
|
||||||
tags=$10::VARCHAR(100)[],
|
headers=$10,
|
||||||
messenger=$11,
|
tags=$11::VARCHAR(100)[],
|
||||||
template_id=$12,
|
messenger=$12,
|
||||||
|
template_id=$13,
|
||||||
updated_at=NOW()
|
updated_at=NOW()
|
||||||
WHERE id = $1 RETURNING id
|
WHERE id = $1 RETURNING id
|
||||||
),
|
),
|
||||||
d AS (
|
d AS (
|
||||||
-- Reset list relationships
|
-- Reset list relationships
|
||||||
DELETE FROM campaign_lists WHERE campaign_id = $1 AND NOT(list_id = ANY($13))
|
DELETE FROM campaign_lists WHERE campaign_id = $1 AND NOT(list_id = ANY($14))
|
||||||
)
|
)
|
||||||
INSERT INTO campaign_lists (campaign_id, list_id, list_name)
|
INSERT INTO campaign_lists (campaign_id, list_id, list_name)
|
||||||
(SELECT $1 as campaign_id, id, name FROM lists WHERE id=ANY($13::INT[]))
|
(SELECT $1 as campaign_id, id, name FROM lists WHERE id=ANY($14::INT[]))
|
||||||
ON CONFLICT (campaign_id, list_id) DO UPDATE SET list_name = EXCLUDED.list_name;
|
ON CONFLICT (campaign_id, list_id) DO UPDATE SET list_name = EXCLUDED.list_name;
|
||||||
|
|
||||||
-- name: update-campaign-counts
|
-- name: update-campaign-counts
|
||||||
|
|
|
@ -78,6 +78,7 @@ CREATE TABLE campaigns (
|
||||||
altbody TEXT NULL,
|
altbody TEXT NULL,
|
||||||
content_type content_type NOT NULL DEFAULT 'richtext',
|
content_type content_type NOT NULL DEFAULT 'richtext',
|
||||||
send_at TIMESTAMP WITH TIME ZONE,
|
send_at TIMESTAMP WITH TIME ZONE,
|
||||||
|
headers JSONB NOT NULL DEFAULT '[]',
|
||||||
status campaign_status NOT NULL DEFAULT 'draft',
|
status campaign_status NOT NULL DEFAULT 'draft',
|
||||||
tags VARCHAR(100)[],
|
tags VARCHAR(100)[],
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue