Compare commits
53 commits
developmen
...
master
Author | SHA1 | Date | |
---|---|---|---|
d563928b0b | |||
|
281072ac1b | ||
|
be55f2ca52 | ||
|
dea056848a | ||
|
b7f94b40d6 | ||
|
3b365701bb | ||
|
62e63fbb80 | ||
|
0a0eb0db4f | ||
|
4e298aca7b | ||
|
50e52207f9 | ||
|
cf6c0e6849 | ||
|
9ca4767b4e | ||
|
d685c8e61f | ||
|
cce887361b | ||
|
6d75ce035d | ||
|
0db686d63c | ||
|
a376dce0c1 | ||
|
c9383cb6da | ||
|
4d25685d69 | ||
|
db9c3271f5 | ||
|
5eb7976a75 | ||
|
47802a7933 | ||
|
cda2beb5f5 | ||
|
e2a9cc9429 | ||
|
c3cf46a717 | ||
|
4353201ab8 | ||
|
d45b1fa762 | ||
|
e74401b25d | ||
|
33f94034e2 | ||
|
fa1bf1c874 | ||
|
de78c587f5 | ||
|
4a446a99f0 | ||
|
2b11a319b4 | ||
|
f2ed0e8ce2 | ||
|
ebd7b1fffb | ||
|
a49a9b2637 | ||
|
5b24186240 | ||
|
d3ad3e5d68 | ||
|
cdaf4b0b16 | ||
|
d5ce6a5d33 | ||
|
dd696d49ac | ||
|
5394ada6ad | ||
|
ce10388f4f | ||
|
f661ba8a6b | ||
|
26b3cd44e8 | ||
|
6bdf074c01 | ||
|
f89ba5a5dd | ||
|
5db5bb73c4 | ||
|
dcf8330929 | ||
|
a774975f45 | ||
|
af8c3202dd | ||
|
4dd754ff82 | ||
|
920f833988 |
27 changed files with 13490 additions and 315 deletions
6
.gitignore
vendored
6
.gitignore
vendored
|
@ -5,9 +5,9 @@ node_modules
|
|||
npm-debug.log
|
||||
package-lock.json
|
||||
.DS_Store
|
||||
config/development.*
|
||||
config/production.*
|
||||
config/test.*
|
||||
config/development*
|
||||
config/production*
|
||||
config/test*
|
||||
workers/reports/config/development.*
|
||||
workers/reports/config/production.*
|
||||
workers/reports/config/test.*
|
||||
|
|
50
Dockerfile
50
Dockerfile
|
@ -1,13 +1,47 @@
|
|||
FROM node:8.6
|
||||
# Mutistaged Node.js Build
|
||||
FROM node:10-alpine as builder
|
||||
|
||||
# Install system dependencies
|
||||
RUN set -ex; \
|
||||
apk add --update --no-cache \
|
||||
make gcc g++ git python
|
||||
|
||||
# Copy package.json dependencies
|
||||
COPY server/package.json /app/server/package.json
|
||||
COPY server/package-lock.json /app/server/package-lock.json
|
||||
COPY client/package.json /app/client/package.json
|
||||
COPY client/package-lock.json /app/client/package-lock.json
|
||||
COPY shared/package.json /app/shared/package.json
|
||||
COPY shared/package-lock.json /app/shared/package-lock.json
|
||||
COPY zone-mta/package.json /app/zone-mta/package.json
|
||||
COPY zone-mta/package-lock.json /app/zone-mta/package-lock.json
|
||||
|
||||
# Install dependencies in each directory
|
||||
RUN cd /app/client && npm install
|
||||
RUN cd /app/shared && npm install --production
|
||||
RUN cd /app/server && npm install --production
|
||||
RUN cd /app/zone-mta && npm install --production
|
||||
|
||||
# First install dependencies
|
||||
COPY ./package.json ./app/
|
||||
WORKDIR /app/
|
||||
ENV NODE_ENV production
|
||||
RUN npm install --no-progress --production && npm install --no-progress passport-ldapjs passport-ldapauth
|
||||
# Later, copy the app files. That improves development speed as buiding the Docker image will not have
|
||||
# to download and install all the NPM dependencies every time there's a change in the source code
|
||||
COPY . /app
|
||||
EXPOSE 3000
|
||||
|
||||
RUN set -ex; \
|
||||
cd /app/client && \
|
||||
npm run build && \
|
||||
rm -rf node_modules
|
||||
|
||||
# Final Image
|
||||
FROM node:10-alpine
|
||||
|
||||
WORKDIR /app/
|
||||
|
||||
# Install system dependencies
|
||||
RUN set -ex; \
|
||||
apk add --update --no-cache \
|
||||
pwgen netcat-openbsd bash imagemagick
|
||||
|
||||
COPY --from=builder /app/ /app/
|
||||
|
||||
EXPOSE 3000 3003 3004
|
||||
ENTRYPOINT ["bash", "/app/docker-entrypoint.sh"]
|
||||
CMD ["node", "index.js"]
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
## Mailtrain v2 beta is available. Check it out here https://github.com/Mailtrain-org/mailtrain/tree/development
|
||||
|
||||
# Mailtrain
|
||||
|
||||
[Mailtrain](http://mailtrain.org) is a self hosted newsletter application built on Node.js (v7+) and MySQL (v5.5+ or MariaDB).
|
||||
|
@ -33,7 +35,7 @@ Depending on how you have configured your system and Docker you may need to prep
|
|||
* 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.
|
||||
* Navigate to [http://localhost:3000/settings](http://localhost:3000/settings) and update service configuration, especially replace the value in `Service Address (URL)` from `localhost` to the actual IP or domain if installed on a server, otherwise e.g. image upload will not work.
|
||||
* Navigate to [http://localhost:3000/users/account](http://localhost:3000/users/account) and update user information and password.
|
||||
|
||||
## Quick Start - Manual Install (any OS that supports Node.js)
|
||||
|
@ -47,7 +49,7 @@ Depending on how you have configured your system and Docker you may need to prep
|
|||
4. Run the server `NODE_ENV=production npm start`
|
||||
5. Open [http://localhost:3000/](http://localhost:3000/)
|
||||
6. Authenticate as `admin`:`test`
|
||||
7. Navigate to [http://localhost:3000/settings](http://localhost:3000/settings) and update service configuration
|
||||
7. Navigate to [http://localhost:3000/settings](http://localhost:3000/settings) and update service configuration, especially replace the value in `Service Address (URL)` from `localhost` to the actual IP or domain if installed on a server, otherwise e.g. image upload will not work.
|
||||
8. Navigate to [http://localhost:3000/users/account](http://localhost:3000/users/account) and update user information and password
|
||||
|
||||
## Read The Docs
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Mailtrain
|
||||
# Mailtrain (v1)
|
||||
|
||||
[Mailtrain](http://mailtrain.org) is a self hosted newsletter application built on Node.js (v7+) and MySQL (v5.5+ or MariaDB).
|
||||
|
||||
|
@ -42,7 +42,7 @@ Check out [ZoneMTA](https://github.com/zone-eu/zone-mta) as an alternative self
|
|||
|
||||
## Cons
|
||||
|
||||
* Beta-grade software. Might or might not work as expected. There are several users with list sizes between 100k and 1M and Mailtrain seems to work for them but YMMV
|
||||
* Beta-grade software. Several users reported success with lists of various sizes (from 100k to 1M) however there is no absolute guarantee it will always work as expected.
|
||||
* Almost no documentation (there are some guides in the [Wiki](https://github.com/Mailtrain-org/mailtrain/wiki))
|
||||
|
||||
## Requirements
|
||||
|
@ -76,13 +76,13 @@ Install script installs and sets up the following:
|
|||
* **logrotate** to rotate Mailtrain log files
|
||||
* **upstart** or **systemd** init script to automatically start and manage Mailtrain process
|
||||
|
||||
After the install script has finished and you see a success message then you should have a Mailtrain instance running at http://yourdomain.com
|
||||
After the install script has finished and you have received "successfully installed" message, you should have a Mailtrain instance running at http://yourdomain.com
|
||||
|
||||
#### Next steps after installation
|
||||
|
||||
##### 1. Change admin password
|
||||
|
||||
Navigate to http://yourdomain.com where yourdomain.com is the address of your server. Click on the Sign In link in the right top corner of the page. Authenticate with the following credentials:
|
||||
Navigate to http://yourdomain.com where yourdomain.com is the address of your server. Click on the **Sign In** link in the right top corner of the page. Authenticate with the following credentials:
|
||||
|
||||
* Username: **admin**
|
||||
* Password: **test**
|
||||
|
@ -91,7 +91,7 @@ Once authenticated, click on your username in the right top corner of the page a
|
|||
|
||||
##### 2. Update page configuration
|
||||
|
||||
If signed in navigate to http://yourdomain.com/settings and check that all email addresses and domain names are correct. Mailtrain default installation comes bundled with [ZoneMTA](https://github.com/zone-eu/zone-mta), so you should be able to send out messages right away. ZoneMTA even handles a lot of bounces (not all kind of bounces though) automatically so you do not have to change anything in the SMTP settings to get going.
|
||||
If signed in, navigate to http://yourdomain.com/settings and check that all email addresses and domain names are correct. Mailtrain default installation comes bundled with [ZoneMTA](https://github.com/zone-eu/zone-mta), so you should be able to send out messages right away. ZoneMTA even handles a lot of bounces (not all kind of bounces though) automatically so you do not have to change anything in the SMTP settings to get going.
|
||||
|
||||
##### 3. Set up SPF
|
||||
|
||||
|
@ -103,9 +103,9 @@ If you are using the bundled ZoneMTA then you can provide a DKIM key to sign all
|
|||
|
||||
##### 5. Set up VERP
|
||||
|
||||
The bundled ZoneMTA can already handle a large amount of bounces if you use it to deliver messages but not all - namely such bounces that happen *after* the recipient MX accepts the message for local delivery. This might happen for example when a user exists, so the MX accepts the message but the quota for that user is checked only when actually storing the message to users' mailbox. Then a bounce message is generated and sent to the original sender which in your case is the mail address you are sending your list messages from. You can catch these messages and mark such recipients manually as bounced but alternatively you can set up a VERP based bounce handler that does this automatically. In this case the sender on the message envelope would not be your actual address but a rewritten bounce address that points to your Mailtrain installation.
|
||||
The bundled ZoneMTA can already handle a large amount of bounces if you use it to deliver messages but not all: namely, such bounces that happen *after* the recipient MX accepts the message for local delivery. This might happen for example when a user exists, so the MX accepts the message but the quota for that user is checked only when actually storing the message to users' mailbox. Then a bounce message is generated and sent to the original sender which in your case is the mail address you are sending your list messages from. You can catch these messages and mark such recipients manually as bounced but alternatively you can set up a VERP based bounce handler that does this automatically. In this case the sender on the message envelope would not be your actual address but a rewritten bounce address that points to your Mailtrain installation.
|
||||
|
||||
To set it up you need to create an additonal DNS MX entry for a bounce domain, eg "bounces.example.com" if you are sending from "example.com". This entry should point to your Mailtrain server IP address. Next you should enable the VERP handling in Mailtrain Settings page.
|
||||
To set it up you need to create an additonal DNS MX entry for a bounce domain, eg "bounces.example.com",if you are sending from "example.com". This entry should point to your Mailtrain server IP address. Next you should enable the VERP handling in Mailtrain Settings page.
|
||||
|
||||
> As ZoneMTA uses envelope sender as the default for DKIM addresses, then if using VERP you need to set up DKIM to your bounce domain instead of sender domain and also store the DKIM key as "bouncedomain.selector.pem" in the ZoneMTA key folder.
|
||||
|
||||
|
@ -123,7 +123,7 @@ With proper SPF, DKIM and PTR records (DMARC wouldn't hurt either) I got perfect
|
|||
|
||||
DKIM, DMARK, SPF and PTR are DNS records which spam filters use to figure out if e-mails were really sent by you (and not by a spammer who tries to conceal his identity to be able to continue send bulks of e-mails people never subscribed for). Assuming that you use zone-mta and your e-mails are to originate from a Mailtrain installation at `mailtrain.example.com` and optionally from `mail.example.net`, to practically set all these records up you will need to:
|
||||
|
||||
1. generate genrate a private and public DKIM key
|
||||
**1.generate a private and public DKIM key**
|
||||
|
||||
```sh
|
||||
mkdir /opt/dkim-keys
|
||||
|
@ -133,7 +133,7 @@ openssl genrsa -out mailtrain.example.com.key 2048 # private key mailtrain.examp
|
|||
openssl rsa -in mailtrain.example.com.key -out mailtrain.example.com.pub -pubout -outform PEM # public key mailtrain.example.com.pub
|
||||
```
|
||||
|
||||
2. add 3 new txt records for the mailtrain.example.com that will most likely similar to the example below:
|
||||
**2.add the three new txt records for the mailtrain.example.com that will most likely look similar to the example below**
|
||||
|
||||
```
|
||||
default._domainkey.mailtrain.example.com TXT "k=rsa; p=[public key in one line];"
|
||||
|
@ -151,10 +151,10 @@ The above steps will have the following effect:
|
|||
|
||||
- all messages sent by Mailtrain / Zone-mta will be signed by the DKIM Private Key (the signature becomes a part of the e-mail)
|
||||
- when a spamfilter encounters this signature, it will look for the **<DKIM selector>**._domainkey.**<DKIM domain>** TXT record, and use the public key stored there to verify that the signature is valid
|
||||
- additionally, the spamfilter will look for a TXT SPF record and will look a if the e-mail was sent from the IP address of mailtrain.example.com or mail.example.net. If the sender IP or domain is different, it will discard the e-mail as spam.
|
||||
- additionally, the spamfilter will look for a TXT SPF record and will look if the e-mail was sent from the IP address of mailtrain.example.com or mail.example.net. If the sender IP or domain is different, it will discard the e-mail as spam.
|
||||
- furthermore, the spamfilter looks for the DMARC record, which tells it what to do with mails that aren't signed with DKIM or which don't have a valid signature. The example above will tell the spamfilter to reject such a mail as well.
|
||||
|
||||
3. You are now almost set. To further confirm that you have full control over your network, the last step is to set up a PTR record, which will give the right answer for a reverse DNS lookup (answer to "what domain name is bound to IP address xxx.xxx.xxx.xxx). If you run your own DNS, you probably know it will look similar to this:
|
||||
You are now almost set. To further confirm that you have full control over your network, the last step is to set up a PTR record, which will give the right answer for a reverse DNS lookup (answer to "what domain name is bound to IP address xxx.xxx.xxx.xxx). If you run your own DNS, you probably know it will look similar to this:
|
||||
|
||||
```
|
||||
10.27/1.110.220.in-addr.arpa. 1800 PTR mailtrain.example.com.
|
||||
|
@ -172,7 +172,7 @@ If you run Mailtrain on a VPS, you will have to find the PTR configuration somew
|
|||
#### Install:
|
||||
|
||||
* 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`
|
||||
* **Note**: depending on how you have configured your system and Docker you may need to prepend the commands below with `sudo`.
|
||||
* **Note**: depending on how you have configured your system and Docker, you may need to prepend the commands below with `sudo`.
|
||||
* 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`, by default it will use the included `docker-compose.yml` file and override some configurations taken from the `docker-compose.override.yml` file.
|
||||
* If you want to use only / copy the `docker-compose.yml` file (for example, if you were deploying with Rancher), you may need to first run `docker-compose build` to make sure your system has a Docker image `mailtrain:latest`.
|
||||
|
@ -221,11 +221,11 @@ Mailtrain uses [node-config](https://github.com/lorenwest/node-config) for confi
|
|||
|
||||
### Running Behind Nginx Proxy
|
||||
|
||||
Edit [mailtrain.nginx](setup/mailtrain-nginx.conf) (update `server_name` directive) and copy it to `/etc/nginx/sites-enabled`
|
||||
Edit [mailtrain.nginx](../setup/mailtrain-nginx.conf) (update `server_name` directive) and copy it to `/etc/nginx/sites-enabled`
|
||||
|
||||
### Running as an Upstart Service in Ubuntu 14.04
|
||||
|
||||
Edit [mailtrain.conf](setup/mailtrain.conf) (update application folder) and copy it to `/etc/init`
|
||||
Edit [mailtrain.conf](../setup/mailtrain.conf) (update application folder) and copy it to `/etc/init`
|
||||
|
||||
## Subscription Widget
|
||||
|
||||
|
@ -273,7 +273,7 @@ If using VERP with iRedMail, see [this post](http://www.iredmail.org/forum/post4
|
|||
|
||||
## Testing
|
||||
|
||||
There is a built in /dev/null server in Mailtrain that you can use to load test your installation. Check the `[testserver]` section in the configuration file for details. By default the test server is disabled. The server uses only cleartext connections, so select "Do not use encryption" in the encryption settings when setting up the server data in Mailtrain.
|
||||
There is a built in /dev/null server in Mailtrain that you can use to load-test your installation. Check the `[testserver]` section in the configuration file for details. By default, the test server is disabled. The server uses only cleartext connections, so select: "Do not use encryption" in the encryption settings when setting up the server data in Mailtrain.
|
||||
|
||||
Additionally you can generate CSV import files with fake subscriber data:
|
||||
|
||||
|
@ -285,7 +285,7 @@ This command generates a CSV file with 100 000 subscriber accounts
|
|||
|
||||
## Translations
|
||||
|
||||
Mailtrain is currently not translated but it supports translations. To add translations you first need to add translation support for the translatable strings. To test if strings are translatable or not, use a fake language with code "zz"
|
||||
Mailtrain is currently not translated but it supports translations. To add translations you first need to add translation support for the translatable strings. To test if strings are translatable or not, use a fake language with the code "zz".
|
||||
|
||||
```toml
|
||||
language="zz"
|
||||
|
@ -327,10 +327,11 @@ Enclose translatable strings to `{{#translate}}` tags
|
|||
|
||||
* To create the translation catalog run `grunt` from command line. This fetches all translatable strings from JavaScript and Handlebars files and merges these into the translation catalog located at */languages/mailtrain.pot*
|
||||
|
||||
* To add a new language use this catalog file as source. Once you want to update your translation file from the updated catalog, then select "Catalogue" -> "Update from POT file..." in POEdit and select mailtrain.pot. This would merge all new translations from the POT file to your PO file.
|
||||
* To add a new language use this catalog file as a source. Once you want to update your translation file from the updated catalog, select "Catalogue" -> "Update from POT file..." in POEdit and select mailtrain.pot. This will merge all the new translations from the POT file to your PO file.
|
||||
|
||||
*If you have saved the PO file in [./languages](./languages) then POEdit should auto generate required MO file whenever you hit save for the PO file.
|
||||
|
||||
* Once you have a correct MO file in the languages folder, then edit Mailtrain config and set ["language" option](https://github.com/Mailtrain-org/mailtrain/blob/ba8bd1212335cb9bd7ba094beb7b5400f35cae6c/config/default.toml#L30-L31) to your language name. If the value is "et" then Mailtrain loads translations from ./languages/et.mo
|
||||
* Once you have a correct MO file in the languages folder, edit Mailtrain config and set ["language" option](https://github.com/Mailtrain-org/mailtrain/blob/ba8bd1212335cb9bd7ba094beb7b5400f35cae6c/config/default.toml#L30-L31) to your language name. If the value is "et" then Mailtrain loads translations from ./languages/et.mo
|
||||
|
||||
> **NB!** For now translation settings are global, so if you have set a translation in config then this applies to all users. An user can't select another translation than the default even if there is a translation file. This is because current Mailtrain code does not provide request context to functions and the functions generating strings do not know which language to use.
|
||||
|
||||
|
|
Binary file not shown.
|
@ -58,7 +58,7 @@ msgstr "Blacklist"
|
|||
|
||||
#: views/blacklist.hbs:4 views/users/api.hbs:55
|
||||
msgid "Add email to blacklist"
|
||||
msgstr "E-Mail zur Blacklist hinzufügen"
|
||||
msgstr "E-Mail Adresse zur Blacklist hinzufügen"
|
||||
|
||||
#: views/blacklist.hbs:5
|
||||
msgid "Add"
|
||||
|
@ -151,7 +151,7 @@ msgstr "SMTP Antwort"
|
|||
|
||||
#: views/campaigns/bounced.hbs:11
|
||||
msgid "Bounce time"
|
||||
msgstr "Bouncen Zeitpunkt"
|
||||
msgstr "Bounce Zeitpunkt"
|
||||
|
||||
#: views/campaigns/campaigns.hbs:3 views/campaigns/create-triggered.hbs:25
|
||||
#: views/campaigns/create.hbs:3 views/campaigns/create.hbs:4
|
||||
|
@ -289,8 +289,8 @@ msgid ""
|
|||
"new entry is found from this feed it is sent to selected list as an email "
|
||||
"message."
|
||||
msgstr ""
|
||||
"RSS-Kampagne setzt einen Tracker gegen den gewählten RSS-Feed. Wenn ein "
|
||||
"neuer Eintrag aus diesem Feed gefunden wird, wird er als E-Mail an die "
|
||||
"Eine RSS-Kampagne setzt einen Tracker auf den gewählten RSS-Feed. Wenn ein "
|
||||
"neuer Eintrag in diesem Feed gefunden wird, wird er als E-Mail an die "
|
||||
"ausgewählte Liste gesendet."
|
||||
|
||||
#: views/campaigns/create-rss.hbs:7 views/campaigns/create-triggered.hbs:6
|
||||
|
@ -363,21 +363,21 @@ msgstr ""
|
|||
#: views/campaigns/edit-triggered.hbs:16 views/campaigns/edit.hbs:17
|
||||
#: views/campaigns/view.hbs:12
|
||||
msgid "Email \"from name\""
|
||||
msgstr "E-Mail \"von\" Name"
|
||||
msgstr "E-Mail \"von Name\""
|
||||
|
||||
#: views/campaigns/create-rss.hbs:16 views/campaigns/create-triggered.hbs:19
|
||||
#: views/campaigns/create.hbs:19 views/campaigns/edit-rss.hbs:19
|
||||
#: views/campaigns/edit-triggered.hbs:17 views/campaigns/edit.hbs:18
|
||||
#: views/settings.hbs:23
|
||||
msgid "This is the name your emails will come from"
|
||||
msgstr "Dies ist Absendernamen Ihrer E-Mails"
|
||||
msgstr "Dies ist der Absendername Ihrer E-Mails"
|
||||
|
||||
#: views/campaigns/create-rss.hbs:17 views/campaigns/create-triggered.hbs:20
|
||||
#: views/campaigns/create.hbs:20 views/campaigns/edit-rss.hbs:20
|
||||
#: views/campaigns/edit-triggered.hbs:18 views/campaigns/edit.hbs:19
|
||||
#: views/campaigns/view.hbs:13
|
||||
msgid "Email \"from\" address"
|
||||
msgstr "E-Mail \"von\" Adresse"
|
||||
msgstr "E-Mail \"von Adresse\""
|
||||
|
||||
#: views/campaigns/create-rss.hbs:18 views/campaigns/create-triggered.hbs:21
|
||||
#: views/campaigns/edit-rss.hbs:21 views/campaigns/edit-triggered.hbs:19
|
||||
|
@ -386,10 +386,16 @@ msgid "This is the address people will send replies to"
|
|||
msgstr "Dies ist die Reply-To Adresse"
|
||||
|
||||
#: views/campaigns/create-rss.hbs:19 views/campaigns/create-triggered.hbs:24
|
||||
#: views/campaigns/create.hbs:26 views/campaigns/edit-rss.hbs:22
|
||||
#: views/campaigns/edit-triggered.hbs:22 views/campaigns/edit.hbs:25
|
||||
msgid "Disable clicked/opened tracking"
|
||||
msgstr "Tracking deaktivieren"
|
||||
#: views/campaigns/create.hbs:28 views/campaigns/edit-rss.hbs:22
|
||||
#: views/campaigns/edit-triggered.hbs:22 views/campaigns/edit.hbs:27
|
||||
msgid "Disable opened tracking"
|
||||
msgstr "Deaktiviere das Tracking beim Öffnen der E-Mail (Zählpixel)"
|
||||
|
||||
#: views/campaigns/create-rss.hbs:20 views/campaigns/create-triggered.hbs:25
|
||||
#: views/campaigns/create.hbs:29 views/campaigns/edit-rss.hbs:23
|
||||
#: views/campaigns/edit-triggered.hbs:23 views/campaigns/edit.hbs:28
|
||||
msgid "Disable clicked tracking"
|
||||
msgstr "Deaktiviere das Tracking beim Klicken von Links"
|
||||
|
||||
#: views/campaigns/create-triggered.hbs:3
|
||||
#: views/campaigns/create-triggered.hbs:4
|
||||
|
@ -410,12 +416,12 @@ msgstr "Vorlage auswählen:"
|
|||
#: views/campaigns/create-triggered.hbs:15 views/campaigns/create.hbs:15
|
||||
msgid "Selecting a template creates a campaign specific copy from it"
|
||||
msgstr ""
|
||||
"Wenn Sie eine Vorlage auswählen, wird eine kampagenspezifische Kopie erstellt"
|
||||
"Wenn Sie eine Vorlage auswählen, wird eine Kopie nur für diese Kampagne erstellt"
|
||||
|
||||
#: views/campaigns/create-triggered.hbs:16 views/campaigns/create.hbs:16
|
||||
msgid "Or alternatively use an URL as the message content source:"
|
||||
msgstr ""
|
||||
"Oder alternativ können Sie eine URL als E-Mail-Inhalts-Quelle verwenden:"
|
||||
"Oder alternativ können Sie eine URL als Quelle für den E-Mail Inhalt verwenden:"
|
||||
|
||||
#: views/campaigns/create-triggered.hbs:17 views/campaigns/create.hbs:17
|
||||
#: views/campaigns/edit-triggered.hbs:25 views/campaigns/edit.hbs:28
|
||||
|
@ -431,13 +437,13 @@ msgstr ""
|
|||
#: views/campaigns/edit-triggered.hbs:20 views/campaigns/edit.hbs:23
|
||||
#: views/campaigns/view.hbs:15
|
||||
msgid "Email \"subject line\""
|
||||
msgstr "E-Mail-Betreffzeile"
|
||||
msgstr "E-Mail Betreffzeile"
|
||||
|
||||
#: views/campaigns/create-triggered.hbs:23 views/campaigns/create.hbs:25
|
||||
#: views/campaigns/edit-triggered.hbs:21 views/campaigns/edit.hbs:24
|
||||
#: views/settings.hbs:27
|
||||
msgid "Keep it relevant and non-spammy"
|
||||
msgstr "Halten Sie den Inhalt relevant und non-spammy"
|
||||
msgstr "Halten Sie den Inhalt relevant und frei von Spam"
|
||||
|
||||
#: views/campaigns/create.hbs:21 views/campaigns/edit.hbs:20
|
||||
msgid ""
|
||||
|
@ -445,7 +451,7 @@ msgid ""
|
|||
"set"
|
||||
msgstr ""
|
||||
"Dies ist die Adresse, an welche Antworten gesendet werden, ausser die "
|
||||
"Kampagnenspezifische reply-to Adresse ist definiert"
|
||||
"Kampagnenspezifische \"reply-to\" Adresse ist definiert"
|
||||
|
||||
#: views/campaigns/create.hbs:22 views/campaigns/edit.hbs:21
|
||||
#: views/campaigns/view.hbs:14
|
||||
|
@ -458,15 +464,23 @@ msgstr ""
|
|||
"Falls gesetzt, ist dies die Adresse an welche E-Mail-Antworten gesendet "
|
||||
"werden"
|
||||
|
||||
#: views/campaigns/create.hbs:26 views/campaigns/edit.hbs:25
|
||||
msgid "Custom unsubscribe (URL)"
|
||||
msgstr "Benutzerdefiniertes Deabonnement (URL)"
|
||||
|
||||
#: views/campaigns/create.hbs:27 views/campaigns/edit.hbs:26
|
||||
msgid "Set a custom unsubscribe url"
|
||||
msgstr "Geben Sie eine eigene URL an, die den Abonnenten zum Abmelden angezeigt wird"
|
||||
|
||||
#: views/campaigns/delivered.hbs:3 views/campaigns/delivered.hbs:4
|
||||
msgid "Delivered info"
|
||||
msgstr "Zustellungs-Info"
|
||||
msgstr "Zustellungs Info"
|
||||
|
||||
#: views/campaigns/delivered.hbs:6
|
||||
msgid "Subscribers who received the message and did not bounce/unsubscribe:"
|
||||
msgstr ""
|
||||
"Abonnenten, welche die Nachricht erhalten haben und nicht bounced oder "
|
||||
"abbestellen:"
|
||||
"Abonnenten, welche die Nachricht erfolgreich erhalten (kein \"bounce\") und "
|
||||
"nicht deabonniert haben:"
|
||||
|
||||
#: views/campaigns/delivered.hbs:11
|
||||
msgid "Delivery time"
|
||||
|
@ -516,7 +530,7 @@ msgstr "Generell"
|
|||
|
||||
#: views/campaigns/edit-triggered.hbs:23 views/campaigns/edit.hbs:26
|
||||
msgid "Template Settings"
|
||||
msgstr "Vorlagen-Einstellungen"
|
||||
msgstr "Vorlagen Einstellungen"
|
||||
|
||||
#: views/campaigns/edit-triggered.hbs:24 views/campaigns/edit.hbs:27
|
||||
msgid "Template URL"
|
||||
|
@ -568,7 +582,7 @@ msgstr "Anzahl Öffnungen"
|
|||
|
||||
#: views/campaigns/unsubscribed.hbs:3 views/campaigns/unsubscribed.hbs:4
|
||||
msgid "Unsubscribed info"
|
||||
msgstr "Abmeldungs-Info"
|
||||
msgstr "Deabonnement Info"
|
||||
|
||||
#: views/campaigns/unsubscribed.hbs:6
|
||||
msgid "Subscribers who unsubscribed:"
|
||||
|
@ -577,7 +591,7 @@ msgstr "Abonnenten welche deabonnierten:"
|
|||
#: views/campaigns/unsubscribed.hbs:11 views/campaigns/view.hbs:28
|
||||
#: views/lists/subscription/import.hbs:10 routes/lists.js:202
|
||||
msgid "Unsubscribed"
|
||||
msgstr "Abbestellt"
|
||||
msgstr "Deabonniert"
|
||||
|
||||
#: views/campaigns/upload-attachment.hbs:7
|
||||
msgid "Upload"
|
||||
|
@ -619,7 +633,7 @@ msgstr "Kampagnen Vorschau als"
|
|||
|
||||
#: views/campaigns/view.hbs:17
|
||||
msgid "Add new test user"
|
||||
msgstr "Neuen Test-User hinzufügen"
|
||||
msgstr "Neuen Testbenutzer hinzufügen"
|
||||
|
||||
#: views/campaigns/view.hbs:18
|
||||
msgid "No test users yet, create one here"
|
||||
|
@ -643,7 +657,7 @@ msgstr "Blacklisted"
|
|||
|
||||
#: views/campaigns/view.hbs:23
|
||||
msgid "List subscribers who blacklisted by global blacklist"
|
||||
msgstr ""
|
||||
msgstr "Abonnenten, die von der globalen Blacklist aufgelistet wurden"
|
||||
|
||||
#: views/campaigns/view.hbs:24 routes/lists.js:202
|
||||
msgid "Bounced"
|
||||
|
@ -685,7 +699,7 @@ msgstr "Abonnenten, die auf einen Link geklickt haben"
|
|||
msgid ""
|
||||
"Are you sure? This action would start sending messages to the selected list"
|
||||
msgstr ""
|
||||
"Sind Sie sicher? Diese Aktion würde mit dem Senden von Nachrichten an die "
|
||||
"Sind Sie sicher? Diese Aktion wird mit dem Senden von Nachrichten an die "
|
||||
"ausgewählte Liste beginnen"
|
||||
|
||||
#: views/campaigns/view.hbs:35
|
||||
|
@ -706,7 +720,7 @@ msgstr "An Abonnenten senden:"
|
|||
|
||||
#: views/campaigns/view.hbs:39
|
||||
msgid "Are you sure? This action would reset scheduling"
|
||||
msgstr "Sind Sie sicher? Diese Aktion würde die Terminierung zurücksetzen"
|
||||
msgstr "Sind Sie sicher? Diese Aktion wird die Terminierung zurücksetzen"
|
||||
|
||||
#: views/campaigns/view.hbs:40
|
||||
msgid "Cancel"
|
||||
|
@ -728,7 +742,7 @@ msgstr "Am senden"
|
|||
msgid ""
|
||||
"Are you sure? This action would resume sending messages to the selected list"
|
||||
msgstr ""
|
||||
"Sind Sie sicher? Diese Aktion würde das Senden von E-Mails an die "
|
||||
"Sind Sie sicher? Diese Aktion wird das Senden von E-Mails an die "
|
||||
"ausgewählte Liste fortsetzen"
|
||||
|
||||
#: views/campaigns/view.hbs:45 views/campaigns/view.hbs:49
|
||||
|
@ -762,7 +776,7 @@ msgid ""
|
|||
"Are you sure? This action would pause sending new entries in RSS feed as "
|
||||
"email messages to the selected list"
|
||||
msgstr ""
|
||||
"Sind Sie sicher? Diese Aktion würde das Senden neuer Einträge des RSS-Feed "
|
||||
"Sind Sie sicher? Diese Aktion wird das Senden neuer Einträge des RSS-Feed "
|
||||
"als E-Mail-Nachrichten an die ausgewählte Liste pausieren"
|
||||
|
||||
#: views/campaigns/view.hbs:55 views/campaigns/view.hbs:59
|
||||
|
@ -830,7 +844,7 @@ msgstr "Ändern Sie Ihr Passwort"
|
|||
#: views/emails/password-reset-text.hbs:2
|
||||
msgid "We have received a password change request for your Mailtrain account:"
|
||||
msgstr ""
|
||||
"Wir haben eine Passwortänderungsanforderung für Ihr Mailtrain-Konto erhalten:"
|
||||
"Wir haben eine Anforderung zur Passwortänderungs für Ihr Mailtrain-Konto erhalten:"
|
||||
|
||||
#: views/emails/password-reset-html.hbs:3
|
||||
#: views/emails/password-reset-text.hbs:3
|
||||
|
@ -843,8 +857,8 @@ msgid ""
|
|||
"If you did not ask to change your password, then you can ignore this email "
|
||||
"and your password will not be changed."
|
||||
msgstr ""
|
||||
"Wenn Sie nicht angefordert haben, Ihr Passwort zu ändern, können Sie diese E-"
|
||||
"Mail ignorieren und Ihr Passwort wird nicht geändert."
|
||||
"Wenn Sie Ihr Passwort nicht ändern wollen, können Sie diese E-"
|
||||
"Mail einfach ignorieren."
|
||||
|
||||
#: views/emails/rss-html.hbs:1 views/emails/stationery-html.hbs:3
|
||||
#: views/emails/stationery-text.hbs:3
|
||||
|
@ -864,7 +878,7 @@ msgstr "Newsletter abbestellen"
|
|||
#: views/emails/rss-html.hbs:3 views/emails/stationery-html.hbs:5
|
||||
#: views/emails/stationery-text.hbs:5
|
||||
msgid "View this email in your browser"
|
||||
msgstr "E-Mail in Browser ansehen"
|
||||
msgstr "E-Mail im Browser ansehen"
|
||||
|
||||
#: views/emails/stationery-html.hbs:1 views/emails/stationery-text.hbs:1
|
||||
msgid "Hey [FIRST_NAME/Customer],"
|
||||
|
@ -885,10 +899,10 @@ msgid ""
|
|||
"API or import from a CSV file. All lists come with support for custom fields "
|
||||
"and merge tags as well."
|
||||
msgstr ""
|
||||
"Mailtrain ermöglicht es Ihnen, auch sehr große Listen zu verwalten. Million "
|
||||
"Mailtrain ermöglicht es Ihnen, auch sehr große Listen zu verwalten. Millionen von "
|
||||
"Abonnenten? Kein Problem. Sie können Abonnenten manuell über die API "
|
||||
"hinzufügen oder aus einer CSV-Datei importieren. Alle Listen kommen mit "
|
||||
"Unterstützung für benutzerdefinierte Felder und Merge-Tags."
|
||||
"hinzufügen oder aus einer CSV-Datei importieren. Für alle Listen können "
|
||||
"zusätzliche Datenfelder und Merge Tags genutzt werden."
|
||||
|
||||
#: views/index.hbs:3 views/index.hbs:7 views/index.hbs:10 views/index.hbs:13
|
||||
#: views/index.hbs:16 views/index.hbs:19 views/index.hbs:22 views/index.hbs:25
|
||||
|
@ -900,7 +914,7 @@ msgstr "Zeige mehr"
|
|||
#: views/lists/fields/edit.hbs:3 views/lists/fields/fields.hbs:3
|
||||
#: views/lists/fields/fields.hbs:5 views/lists/view.hbs:6
|
||||
msgid "Custom Fields"
|
||||
msgstr "Felder"
|
||||
msgstr "Zusätzliche Datenfelder"
|
||||
|
||||
#: views/index.hbs:5
|
||||
msgid ""
|
||||
|
@ -908,8 +922,8 @@ msgid ""
|
|||
"Every custom field can be included in the generated newsletters through "
|
||||
"merge tags."
|
||||
msgstr ""
|
||||
"Textfelder, Nummern, Dropdowns oder Checkboxen, Mailtrain hat sie alle. "
|
||||
"Jedes benutzerdefinierte Feld kann in den generierten Newslettern mittels "
|
||||
"Textfelder, Nummern, Dropdowns oder Checkboxen, Mailtrain bietet all das. "
|
||||
"Jedes zusätzliche Feld kann in den generierten Newslettern über "
|
||||
"Merge-Tags eingebunden werden."
|
||||
|
||||
#: views/index.hbs:6
|
||||
|
@ -918,7 +932,7 @@ msgstr "Mailtrain bietet auch benutzerdefinierte Formulare."
|
|||
|
||||
#: views/index.hbs:8
|
||||
msgid "List Segmentation"
|
||||
msgstr "Segmentierung"
|
||||
msgstr "Listen Segmentierung"
|
||||
|
||||
#: views/index.hbs:9
|
||||
msgid ""
|
||||
|
@ -939,13 +953,13 @@ msgid ""
|
|||
"then Mailtrain auto-generates a new campaign using entry data as message "
|
||||
"contents and sends it to selected subscribers."
|
||||
msgstr ""
|
||||
"Mailtrain kann RSS-Feeds verfolgen, und wenn ein neuer Eintrag in einem Feed "
|
||||
"erkannt wird, sendet Mailtrain automatisch eine neue Kampagne mit dem RSS-"
|
||||
"Beitrag als Nachrichteninhalt an die ausgewählte Liste."
|
||||
"Mailtrain kann RSS-Feeds verfolgen und bei neuen Einträgen automatisch "
|
||||
"eine neue Kampagne mit dem RSS-Beitrag als Nachrichteninhalt an die "
|
||||
"ausgewählte Liste senden."
|
||||
|
||||
#: views/index.hbs:14
|
||||
msgid "GPG Encryption"
|
||||
msgstr "GPG-Verschlüsselung"
|
||||
msgstr "GPG Verschlüsselung"
|
||||
|
||||
#: views/index.hbs:15
|
||||
msgid ""
|
||||
|
@ -954,7 +968,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 der Liste zu empfangen."
|
||||
"alle E-Mails verschlüsselt zu erhalten."
|
||||
|
||||
#: views/index.hbs:17
|
||||
msgid "Click Stats"
|
||||
|
@ -984,7 +998,7 @@ msgstr ""
|
|||
|
||||
#: views/index.hbs:23
|
||||
msgid "Send via Any Provider"
|
||||
msgstr "Sende mit"
|
||||
msgstr "Sende mit beliebigen Anbietern"
|
||||
|
||||
#: views/index.hbs:24
|
||||
msgid ""
|
||||
|
@ -1015,7 +1029,7 @@ msgstr ""
|
|||
|
||||
#: views/index.hbs:29
|
||||
msgid "Donate to Author"
|
||||
msgstr "Dem Autor Spenden"
|
||||
msgstr "Dem Autor spenden"
|
||||
|
||||
#: views/index.hbs:30
|
||||
msgid "Mailtrain is available under GPLv3 license and completely open source."
|
||||
|
@ -1127,6 +1141,15 @@ msgstr "Name der Liste"
|
|||
msgid "Allow public users to subscribe themselves"
|
||||
msgstr "Allen erlauben, diese Liste selbst zu abonnieren"
|
||||
|
||||
#: views/lists/create.hbs:11 views/lists/edit.hbs:17
|
||||
msgid "Unsubscription"
|
||||
msgstr "Deabonnement"
|
||||
|
||||
#: views/lists/create.hbs:12 views/lists/edit.hbs:18
|
||||
msgid "Select how an unsuscription request by subscriber is handled."
|
||||
msgstr ""
|
||||
"Wählen Sie, welche Schritte zum Abmelden von der Liste notwendig sein sollen."
|
||||
|
||||
#: views/lists/edit.hbs:3 views/lists/edit.hbs:4 views/lists/view.hbs:8
|
||||
msgid "Edit List"
|
||||
msgstr "Liste bearbeiten"
|
||||
|
@ -1145,7 +1168,7 @@ msgstr "Diese Listen ID wird den Abonnenten dargestellt"
|
|||
|
||||
#: views/lists/edit.hbs:12
|
||||
msgid "Custom Form"
|
||||
msgstr "Formular"
|
||||
msgstr "Benutzerdefiniertes Formular"
|
||||
|
||||
#: views/lists/edit.hbs:13 views/lists/forms/forms.hbs:11
|
||||
msgid "Default Mailtrain Form"
|
||||
|
@ -1153,11 +1176,18 @@ msgstr "Standard Mailtrain Formular"
|
|||
|
||||
#: views/lists/edit.hbs:14
|
||||
msgid ""
|
||||
"The custom form used for this list. You can create a form <a href=\"/forms/"
|
||||
"{{id}}/create\">here</a>."
|
||||
"The custom form used for this list. You can create a form <a href=\"/forms/{{id}}/create\">here</a>."
|
||||
msgstr ""
|
||||
"Das Standard-Formular dieser Liste. Wenn Sie ein Formular erstellt möchten, "
|
||||
"klicken Sie <a href=\"/forms/{{id}}/create\">hier</a>."
|
||||
"Das Standard-Formular dieser Liste. Wenn Sie ein benutzerdefiniertes Formular "
|
||||
"erstellen möchten, klicken Sie <a href=\"/forms/{{id}}/create\">hier</a>."
|
||||
|
||||
#: views/lists/edit.hbs:19
|
||||
msgid "Unsubscribe Header"
|
||||
msgstr "Deabonnement Header"
|
||||
|
||||
#: views/lists/edit.hbs:20
|
||||
msgid "Do not send List-Unsubscribe headers"
|
||||
msgstr "Sende keine Header zum Abmelden von der Liste"
|
||||
|
||||
#: views/lists/edit.hbs:16
|
||||
msgid "Delete List"
|
||||
|
@ -1169,7 +1199,7 @@ msgstr "Feld erstellen"
|
|||
|
||||
#: views/lists/fields/create.hbs:5 views/lists/fields/fields.hbs:4
|
||||
msgid "Create Custom Field"
|
||||
msgstr "Feld erstellen"
|
||||
msgstr "zusäzliches Feld erstellen"
|
||||
|
||||
#: views/lists/fields/create.hbs:6 views/lists/fields/create.hbs:7
|
||||
#: views/lists/fields/edit.hbs:7 views/lists/fields/edit.hbs:8
|
||||
|
@ -1247,7 +1277,7 @@ msgstr "Dropdown"
|
|||
#: views/lists/fields/create.hbs:23 views/lists/fields/edit.hbs:24
|
||||
#: lib/models/fields.js:22
|
||||
msgid "Radio Buttons"
|
||||
msgstr "Radio Knöpfe"
|
||||
msgstr "Radio Buttons"
|
||||
|
||||
#: views/lists/fields/create.hbs:24 views/lists/fields/edit.hbs:25
|
||||
#: lib/models/fields.js:23
|
||||
|
@ -1270,7 +1300,7 @@ msgstr "Erforderlich für Gruppenoptionen"
|
|||
#: views/lists/fields/edit.hbs:35 views/lists/fields/edit.hbs:36
|
||||
#: views/lists/fields/fields.hbs:9
|
||||
msgid "Default merge tag value"
|
||||
msgstr "Standard-Merge-Tag-Wert"
|
||||
msgstr "Standard Wert für Merge-Tag"
|
||||
|
||||
#: views/lists/fields/create.hbs:32 views/lists/fields/edit.hbs:34
|
||||
msgid ""
|
||||
|
@ -1305,7 +1335,7 @@ msgstr "Feld bearbeiten"
|
|||
|
||||
#: views/lists/fields/edit.hbs:5
|
||||
msgid "Edit Custom Field"
|
||||
msgstr "Feld bearbeiten"
|
||||
msgstr "Zusätzliches Feld bearbeiten"
|
||||
|
||||
#: views/lists/fields/edit.hbs:6
|
||||
msgid "Back to fields"
|
||||
|
@ -1347,7 +1377,7 @@ msgstr "Bearbeiten"
|
|||
#: views/lists/forms/forms.hbs:3 views/lists/forms/forms.hbs:5
|
||||
#: views/lists/view.hbs:5
|
||||
msgid "Custom Forms"
|
||||
msgstr "Formulare"
|
||||
msgstr "Benutzerdefinierte Formulare"
|
||||
|
||||
#: views/lists/forms/create.hbs:4
|
||||
msgid "Create Form"
|
||||
|
@ -1355,7 +1385,7 @@ msgstr "Formular erstellen"
|
|||
|
||||
#: views/lists/forms/create.hbs:5 views/lists/forms/forms.hbs:4
|
||||
msgid "Create Custom Form"
|
||||
msgstr "Formular erstellen"
|
||||
msgstr "Benutzerdefiniertes Formular erstellen"
|
||||
|
||||
#: views/lists/forms/create.hbs:6 views/lists/forms/create.hbs:7
|
||||
#: views/lists/forms/edit.hbs:7 views/lists/forms/edit.hbs:8
|
||||
|
@ -1807,7 +1837,7 @@ msgstr "Deaktiviert"
|
|||
|
||||
#: views/lists/view.hbs:3
|
||||
msgid "Subscription Form"
|
||||
msgstr "Abonnement-Formular"
|
||||
msgstr "Abonnement Formular"
|
||||
|
||||
#: views/lists/view.hbs:4
|
||||
msgid "List Actions"
|
||||
|
@ -1838,7 +1868,7 @@ msgstr "Filter"
|
|||
|
||||
#: views/lists/view.hbs:17
|
||||
msgid "Subscriptions"
|
||||
msgstr "Abonnemente"
|
||||
msgstr "Abonnements"
|
||||
|
||||
#: views/lists/view.hbs:18
|
||||
msgid "Imports"
|
||||
|
@ -2107,7 +2137,7 @@ msgstr "Wenn markiert zeigt der Editor HTML-Code ohne Vorschau an"
|
|||
|
||||
#: views/settings.hbs:11
|
||||
msgid "Disable subscription confirmation messages"
|
||||
msgstr "Abonnement-Bestätigungsmeldungen deaktivieren"
|
||||
msgstr "Bestätigungsmeldungen für Abonnements deaktivieren"
|
||||
|
||||
#: views/settings.hbs:12
|
||||
msgid ""
|
||||
|
@ -2785,6 +2815,10 @@ msgstr "Optionale Kommentare zu dieser Vorlage"
|
|||
msgid "Back to templates"
|
||||
msgstr "Zurück zu Vorlagen"
|
||||
|
||||
#: views/templates/edit.hbs:11
|
||||
msgid "Duplicate"
|
||||
msgstr "Duplizieren"
|
||||
|
||||
#: views/triggers/create-select.hbs:2 views/triggers/create.hbs:2
|
||||
#: views/triggers/edit.hbs:2 views/triggers/triggered.hbs:2
|
||||
#: views/triggers/triggers.hbs:2 views/triggers/triggers.hbs:4
|
||||
|
@ -3072,7 +3106,7 @@ msgstr ""
|
|||
|
||||
#: views/users/api.hbs:28
|
||||
msgid "Remove subscription"
|
||||
msgstr "Abonement entfernen"
|
||||
msgstr "Abonnement entfernen"
|
||||
|
||||
#: views/users/api.hbs:29
|
||||
msgid "This API call marks a subscription as unsubscribed"
|
||||
|
@ -3493,7 +3527,7 @@ msgstr "Abonnement-ID fehlt"
|
|||
|
||||
#: lib/models/subscriptions.js:499
|
||||
msgid "Missing Subscription email address"
|
||||
msgstr "Abonnement-E-Mail-Adresse fehlt"
|
||||
msgstr "Abonnement E-Mail Adresse fehlt"
|
||||
|
||||
#: lib/models/subscriptions.js:578 lib/models/subscriptions.js:827
|
||||
#: lib/models/subscriptions.js:1090
|
||||
|
@ -4108,6 +4142,33 @@ msgstr "Import gestartet"
|
|||
msgid "Import restarted"
|
||||
msgstr "Import neu gestartet"
|
||||
|
||||
#: routes/lists.js:797
|
||||
msgid "One-step (i.e. no email with confirmation link)"
|
||||
msgstr "Ein Schritt ohne Formular (direkt nach dem Klicken auf den Link erfolgt die Abmeldung)"
|
||||
|
||||
#: routes/lists.js:803
|
||||
msgid ""
|
||||
"One-step with unsubscription form (i.e. no email with confirmation link)"
|
||||
msgstr ""
|
||||
"Ein Schritt mit Formular (nach dem Klick wird ein Formular mit der E-Mail Adresse angezeigt)"
|
||||
|
||||
#: routes/lists.js:809
|
||||
msgid "Two-step (i.e. an email with confirmation link will be sent)"
|
||||
msgstr "Zwei Schritte ohne Formular (direkt nach dem Klick auf den Link wird eine E-Mail mit Bestätigungslink versendet)"
|
||||
|
||||
#: routes/lists.js:815
|
||||
msgid ""
|
||||
"Two-step with unsubscription form (i.e. an email with confirmation link will "
|
||||
"be sent)"
|
||||
msgstr ""
|
||||
"Zwei Schritte mit Formular (nach dem Klick wird ein Formular mit der E-Mail Adresse " "angezeigt, dann wird eine E-Mail mit Bestätigungslink versendet)"
|
||||
|
||||
#: routes/lists.js:821
|
||||
msgid ""
|
||||
"Manual (i.e. unsubscription has to be performed by the list administrator)"
|
||||
msgstr ""
|
||||
"Manuell (die Abmeldung muss durch den Listen Admin vorgenommen werden)"
|
||||
|
||||
#: routes/report-templates.js:246
|
||||
msgid "Could not create report template"
|
||||
msgstr "Report-Vorlage konnte nicht erstellt werden"
|
||||
|
|
BIN
languages/fr_FR.mo
Normal file
BIN
languages/fr_FR.mo
Normal file
Binary file not shown.
4796
languages/fr_FR.po
Normal file
4796
languages/fr_FR.po
Normal file
File diff suppressed because it is too large
Load diff
BIN
languages/hu_HU.mo
Normal file
BIN
languages/hu_HU.mo
Normal file
Binary file not shown.
4521
languages/hu_HU.po
Normal file
4521
languages/hu_HU.po
Normal file
File diff suppressed because it is too large
Load diff
BIN
languages/pl_PL.mo
Normal file
BIN
languages/pl_PL.mo
Normal file
Binary file not shown.
3739
languages/pl_PL.po
Normal file
3739
languages/pl_PL.po
Normal file
File diff suppressed because it is too large
Load diff
|
@ -60,10 +60,10 @@ module.exports.statsClickedSubscribersByColumn = (campaign, linkId, request, col
|
|||
return callback(err);
|
||||
}
|
||||
|
||||
let query_template = 'SELECT %s AS data, COUNT(*) AS cnt FROM `subscription__%d` JOIN `campaign_tracker__%d` ON `campaign_tracker__%d`.`list`=%d AND `campaign_tracker__%d`.`subscriber`=`subscription__%d`.`id` AND `campaign_tracker__%d`.`link`=%d GROUP BY `%s` ORDER BY COUNT(`%s`) DESC, `%s`';
|
||||
let query = util.format(query_template, column, campaign.list, campaign.id, campaign.id, campaign.list, campaign.id, campaign.list, campaign.id, linkId, column, column, column);
|
||||
let query_template = 'SELECT ?? AS data, COUNT(*) AS cnt FROM `subscription__%d` JOIN `campaign_tracker__%d` ON `campaign_tracker__%d`.`list`=%d AND `campaign_tracker__%d`.`subscriber`=`subscription__%d`.`id` AND `campaign_tracker__%d`.`link`=%d GROUP BY ?? ORDER BY COUNT(??) DESC, ??';
|
||||
let query = util.format(query_template, campaign.list, campaign.id, campaign.id, campaign.list, campaign.id, campaign.list, campaign.id, linkId);
|
||||
|
||||
connection.query(query, (err, rows) => {
|
||||
connection.query(query, [column, column, column, column], (err, rows) => {
|
||||
connection.release();
|
||||
if (err) {
|
||||
return callback(err);
|
||||
|
@ -365,6 +365,17 @@ module.exports.getLinks = (id, linkId, callback) => {
|
|||
});
|
||||
};
|
||||
|
||||
module.exports.duplicate = (id, callback) => module.exports.get(id, true, (err, campaign) => {
|
||||
if (err) {
|
||||
return callback(err);
|
||||
}
|
||||
if (!campaign) {
|
||||
return callback(new Error(_('Campaign does not exist')));
|
||||
}
|
||||
campaign.name = campaign.name + ' Copy';
|
||||
return module.exports.create(campaign, false, callback);
|
||||
});
|
||||
|
||||
module.exports.create = (campaign, opts, callback) => {
|
||||
|
||||
campaign = tools.convertKeys(campaign);
|
||||
|
|
|
@ -9,7 +9,7 @@ let Handlebars = require('handlebars');
|
|||
let _ = require('../translate')._;
|
||||
let util = require('util');
|
||||
|
||||
let allowedKeys = ['name', 'key', 'default_value', 'group', 'group_template', 'visible'];
|
||||
let allowedKeys = ['name', 'description', 'key', 'default_value', 'group', 'group_template', 'visible'];
|
||||
let allowedTypes;
|
||||
|
||||
module.exports.grouped = ['radio', 'checkbox', 'dropdown'];
|
||||
|
@ -133,10 +133,11 @@ module.exports.create = (listId, field, callback) => {
|
|||
field.group = null;
|
||||
}
|
||||
|
||||
field.description = (field.description || '').toString().trim() || null;
|
||||
field.defaultValue = (field.defaultValue || '').toString().trim() || null;
|
||||
field.groupTemplate = (field.groupTemplate || '').toString().trim() || null;
|
||||
|
||||
addCustomField(listId, field.name, field.defaultValue, field.type, field.group, field.groupTemplate, field.visible, callback);
|
||||
addCustomField(listId, field.name, field.description, field.defaultValue, field.type, field.group, field.groupTemplate, field.visible, callback);
|
||||
};
|
||||
|
||||
module.exports.update = (id, updates, callback) => {
|
||||
|
@ -157,6 +158,7 @@ module.exports.update = (id, updates, callback) => {
|
|||
updates.key = slugify(updates.key, '_').toUpperCase();
|
||||
}
|
||||
|
||||
updates.description = (updates.description || '').toString().trim() || null;
|
||||
updates.defaultValue = (updates.defaultValue || '').toString().trim() || null;
|
||||
updates.groupTemplate = (updates.groupTemplate || '').toString().trim() || null;
|
||||
|
||||
|
@ -277,7 +279,7 @@ module.exports.delete = (fieldId, callback) => {
|
|||
});
|
||||
};
|
||||
|
||||
function addCustomField(listId, name, defaultValue, type, group, groupTemplate, visible, callback) {
|
||||
function addCustomField(listId, name, description, defaultValue, type, group, groupTemplate, visible, callback) {
|
||||
type = (type || '').toString().trim().toLowerCase();
|
||||
group = Number(group) || null;
|
||||
listId = Number(listId) || 0;
|
||||
|
@ -314,8 +316,8 @@ function addCustomField(listId, name, defaultValue, type, group, groupTemplate,
|
|||
column = ('custom_' + slugify(name, '_') + '_' + shortid.generate()).toLowerCase().replace(/[^a-z0-9_]/g, '');
|
||||
}
|
||||
|
||||
let query = 'INSERT INTO custom_fields (`list`, `name`, `key`,`default_value`, `type`, `group`, `group_template`, `column`, `visible`) VALUES(?,?,?,?,?,?,?,?,?)';
|
||||
connection.query(query, [listId, name, key, defaultValue, type, group, groupTemplate, column, visible ? 1 : 0], (err, result) => {
|
||||
let query = 'INSERT INTO custom_fields (`list`, `name`, `description`, `key`,`default_value`, `type`, `group`, `group_template`, `column`, `visible`) VALUES(?,?,?,?,?,?,?,?,?,?)';
|
||||
connection.query(query, [listId, name, description, key, defaultValue, type, group, groupTemplate, column, visible ? 1 : 0], (err, result) => {
|
||||
|
||||
if (err) {
|
||||
connection.release();
|
||||
|
@ -409,6 +411,7 @@ module.exports.getRow = (fieldList, values, useDate, showAll, onlyExisting) => {
|
|||
id: field.id,
|
||||
type: field.type,
|
||||
name: field.name,
|
||||
description: field.description,
|
||||
column: field.column,
|
||||
value: (valueList[field.column] || '').toString().trim(),
|
||||
visible: !!field.visible,
|
||||
|
@ -439,6 +442,7 @@ module.exports.getRow = (fieldList, values, useDate, showAll, onlyExisting) => {
|
|||
id: field.id,
|
||||
type: field.type,
|
||||
name: field.name,
|
||||
description: field.description,
|
||||
column: field.column,
|
||||
value: (valueList[field.column] || '').toString().trim(),
|
||||
visible: !!field.visible,
|
||||
|
@ -455,6 +459,7 @@ module.exports.getRow = (fieldList, values, useDate, showAll, onlyExisting) => {
|
|||
id: field.id,
|
||||
type: field.type,
|
||||
name: field.name,
|
||||
description: field.description,
|
||||
column: field.column,
|
||||
value: Number(valueList[field.column]) || 0,
|
||||
visible: !!field.visible,
|
||||
|
@ -474,6 +479,7 @@ module.exports.getRow = (fieldList, values, useDate, showAll, onlyExisting) => {
|
|||
id: field.id,
|
||||
type: field.type,
|
||||
name: field.name,
|
||||
description: field.description,
|
||||
visible: !!field.visible,
|
||||
key: 'group-g' + field.id,
|
||||
mergeTag: field.key,
|
||||
|
@ -495,6 +501,7 @@ module.exports.getRow = (fieldList, values, useDate, showAll, onlyExisting) => {
|
|||
return {
|
||||
type: subField.type,
|
||||
name: subField.name,
|
||||
description: subField.description,
|
||||
column: subField.column,
|
||||
value: valueList[subField.column] ? 1 : 0,
|
||||
visible: !!subField.visible,
|
||||
|
@ -572,6 +579,7 @@ module.exports.getRow = (fieldList, values, useDate, showAll, onlyExisting) => {
|
|||
id: field.id,
|
||||
type: field.type,
|
||||
name: field.name,
|
||||
description: field.description,
|
||||
column: field.column,
|
||||
value: useDate ? value : formatted,
|
||||
visible: !!field.visible,
|
||||
|
|
|
@ -574,11 +574,12 @@ module.exports.getQuery = (id, prefix, callback) => {
|
|||
|
||||
segment.rules.forEach(rule => {
|
||||
switch (rule.columnType.type) {
|
||||
case 'string':
|
||||
case 'string': {
|
||||
let condition = rule.value.negate ? 'NOT LIKE' : 'LIKE';
|
||||
query.push(prefix + '`' + rule.columnType.column + '` ' + condition + ' ?');
|
||||
values.push(rule.value.value);
|
||||
break;
|
||||
}
|
||||
case 'boolean':
|
||||
query.push(prefix + '`' + rule.columnType.column + '` = ?');
|
||||
values.push(rule.value.value);
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
{
|
||||
"schemaVersion": 33
|
||||
"schemaVersion": 34
|
||||
}
|
||||
|
|
|
@ -106,6 +106,7 @@
|
|||
"openpgp": "^2.5.11",
|
||||
"passport": "^0.4.0",
|
||||
"passport-local": "^1.0.0",
|
||||
"passport-ldapauth": "^2.0.0",
|
||||
"premailer-api": "^1.0.4",
|
||||
"redfour": "^1.0.2",
|
||||
"redis": "^2.8.0",
|
||||
|
|
293
routes/api.js
293
routes/api.js
|
@ -12,29 +12,27 @@ let log = require('npmlog');
|
|||
let router = new express.Router();
|
||||
let mailHelpers = require('../lib/subscription-mail-helpers');
|
||||
|
||||
router.all('/*', (req, res, next) => {
|
||||
if (!req.query.access_token) {
|
||||
res.status(403);
|
||||
const handleErrorResponse = (res, log, err, code = 500, message = false) => {
|
||||
if (typeof err != 'undefined')
|
||||
log.error('API', err);
|
||||
res.status(code);
|
||||
return res.json({
|
||||
error: 'Missing access_token',
|
||||
error: message || err.message || err,
|
||||
data: []
|
||||
});
|
||||
}
|
||||
|
||||
router.all('/*', (req, res, next) => {
|
||||
if (!req.query.access_token) {
|
||||
return handleErrorResponse(res, log, false, 403, 'Missing access_token');
|
||||
}
|
||||
|
||||
users.findByAccessToken(req.query.access_token, (err, user) => {
|
||||
if (err) {
|
||||
res.status(500);
|
||||
return res.json({
|
||||
error: err.message || err,
|
||||
data: []
|
||||
});
|
||||
return handleErrorResponse(res, log, err);
|
||||
}
|
||||
if (!user) {
|
||||
res.status(403);
|
||||
return res.json({
|
||||
error: 'Invalid or expired access_token',
|
||||
data: []
|
||||
});
|
||||
return handleErrorResponse(res, log, false, 403, 'Invalid or expired access_token');
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
@ -48,35 +46,17 @@ router.post('/subscribe/:listId', (req, res) => {
|
|||
});
|
||||
lists.getByCid(req.params.listId, (err, list) => {
|
||||
if (err) {
|
||||
log.error('API', err);
|
||||
res.status(500);
|
||||
return res.json({
|
||||
error: err.message || err,
|
||||
data: []
|
||||
});
|
||||
return handleErrorResponse(res, log, false, 403, 'Invalid or expired access_token');
|
||||
}
|
||||
if (!list) {
|
||||
res.status(404);
|
||||
return res.json({
|
||||
error: 'Selected listId not found',
|
||||
data: []
|
||||
});
|
||||
return handleErrorResponse(res, log, false, 404, 'Selected listId not found');
|
||||
}
|
||||
if (!input.EMAIL) {
|
||||
res.status(400);
|
||||
return res.json({
|
||||
error: 'Missing EMAIL',
|
||||
data: []
|
||||
});
|
||||
return handleErrorResponse(res, log, false, 400, 'Missing EMAIL');
|
||||
}
|
||||
tools.validateEmail(input.EMAIL, false, err => {
|
||||
if (err) {
|
||||
log.error('API', err);
|
||||
res.status(400);
|
||||
return res.json({
|
||||
error: err.message || err,
|
||||
data: []
|
||||
});
|
||||
return handleErrorResponse(res, log, err, 400);
|
||||
}
|
||||
|
||||
let subscription = {
|
||||
|
@ -132,22 +112,12 @@ router.post('/subscribe/:listId', (req, res) => {
|
|||
|
||||
confirmations.addConfirmation(list.id, 'subscribe', req.ip, data, (err, confirmCid) => {
|
||||
if (err) {
|
||||
log.error('API', err);
|
||||
res.status(500);
|
||||
return res.json({
|
||||
error: err.message || err,
|
||||
data: []
|
||||
});
|
||||
return handleErrorResponse(res, log, err);
|
||||
}
|
||||
|
||||
mailHelpers.sendConfirmSubscription(list, input.EMAIL, confirmCid, subscription, (err) => {
|
||||
if (err) {
|
||||
log.error('API', err);
|
||||
res.status(500);
|
||||
return res.json({
|
||||
error: err.message || err,
|
||||
data: []
|
||||
});
|
||||
return handleErrorResponse(res, log, err);
|
||||
}
|
||||
|
||||
res.status(200);
|
||||
|
@ -161,12 +131,7 @@ router.post('/subscribe/:listId', (req, res) => {
|
|||
} else {
|
||||
subscriptions.insert(list.id, meta, subscription, (err, response) => {
|
||||
if (err) {
|
||||
log.error('API', err);
|
||||
res.status(500);
|
||||
return res.json({
|
||||
error: err.message || err,
|
||||
data: []
|
||||
});
|
||||
return handleErrorResponse(res, log, err);
|
||||
}
|
||||
res.status(200);
|
||||
res.json({
|
||||
|
@ -188,51 +153,26 @@ router.post('/unsubscribe/:listId', (req, res) => {
|
|||
});
|
||||
lists.getByCid(req.params.listId, (err, list) => {
|
||||
if (err) {
|
||||
res.status(500);
|
||||
return res.json({
|
||||
error: err.message || err,
|
||||
data: []
|
||||
});
|
||||
return handleErrorResponse(res, log, err);
|
||||
}
|
||||
if (!list) {
|
||||
res.status(404);
|
||||
return res.json({
|
||||
error: 'Selected listId not found',
|
||||
data: []
|
||||
});
|
||||
return handleErrorResponse(res, log, false, 404, 'Selected listId not found');
|
||||
}
|
||||
if (!input.EMAIL) {
|
||||
res.status(400);
|
||||
return res.json({
|
||||
error: 'Missing EMAIL',
|
||||
data: []
|
||||
});
|
||||
return handleErrorResponse(res, log, false, 400, 'Missing EMAIL');
|
||||
}
|
||||
|
||||
subscriptions.getByEmail(list.id, input.EMAIL, (err, subscription) => {
|
||||
if (err) {
|
||||
res.status(500);
|
||||
return res.json({
|
||||
error: err.message || err,
|
||||
data: []
|
||||
});
|
||||
return handleErrorResponse(res, log, err);
|
||||
}
|
||||
|
||||
if (!subscription) {
|
||||
res.status(404);
|
||||
return res.json({
|
||||
error: 'Subscription with given email not found',
|
||||
data: []
|
||||
});
|
||||
return handleErrorResponse(res, log, false, 404, 'Subscription with given email not found');
|
||||
}
|
||||
|
||||
subscriptions.changeStatus(list.id, subscription.id, false, subscriptions.Status.UNSUBSCRIBED, (err, found) => {
|
||||
if (err) {
|
||||
res.status(500);
|
||||
return res.json({
|
||||
error: err.message || err,
|
||||
data: []
|
||||
});
|
||||
return handleErrorResponse(res, log, err);
|
||||
}
|
||||
res.status(200);
|
||||
res.json({
|
||||
|
@ -253,55 +193,27 @@ router.post('/delete/:listId', (req, res) => {
|
|||
});
|
||||
lists.getByCid(req.params.listId, (err, list) => {
|
||||
if (err) {
|
||||
res.status(500);
|
||||
return res.json({
|
||||
error: err.message || err,
|
||||
data: []
|
||||
});
|
||||
return handleErrorResponse(res, log, err);
|
||||
}
|
||||
if (!list) {
|
||||
res.status(404);
|
||||
return res.json({
|
||||
error: 'Selected listId not found',
|
||||
data: []
|
||||
});
|
||||
return handleErrorResponse(res, log, false, 404, 'Selected listId not found');
|
||||
}
|
||||
if (!input.EMAIL) {
|
||||
res.status(400);
|
||||
return res.json({
|
||||
error: 'Missing EMAIL',
|
||||
data: []
|
||||
});
|
||||
return handleErrorResponse(res, log, false, 400, 'Missing EMAIL');
|
||||
}
|
||||
subscriptions.getByEmail(list.id, input.EMAIL, (err, subscription) => {
|
||||
if (err) {
|
||||
res.status(500);
|
||||
return res.json({
|
||||
error: err.message || err,
|
||||
data: []
|
||||
});
|
||||
return handleErrorResponse(res, log, err);
|
||||
}
|
||||
if (!subscription) {
|
||||
res.status(404);
|
||||
return res.json({
|
||||
error: 'Subscription not found',
|
||||
data: []
|
||||
});
|
||||
return handleErrorResponse(res, log, false, 404, 'Subscription not found');
|
||||
}
|
||||
subscriptions.delete(list.id, subscription.cid, (err, subscription) => {
|
||||
if (err) {
|
||||
res.status(500);
|
||||
return res.json({
|
||||
error: err.message || err,
|
||||
data: []
|
||||
});
|
||||
return handleErrorResponse(res, log, err);
|
||||
}
|
||||
if (!subscription) {
|
||||
res.status(404);
|
||||
return res.json({
|
||||
error: 'Subscription not found',
|
||||
data: []
|
||||
});
|
||||
return handleErrorResponse(res, log, false, 404, 'Subscription not found');
|
||||
}
|
||||
res.status(200);
|
||||
res.json({
|
||||
|
@ -321,19 +233,11 @@ router.get('/subscriptions/:listId', (req, res) => {
|
|||
|
||||
lists.getByCid(req.params.listId, (err, list) => {
|
||||
if (err) {
|
||||
res.status(500);
|
||||
return res.json({
|
||||
error: err.message || err,
|
||||
data: []
|
||||
});
|
||||
return handleErrorResponse(res, log, err);
|
||||
}
|
||||
subscriptions.list(list.id, start, limit, (err, rows, total) => {
|
||||
if (err) {
|
||||
res.status(500);
|
||||
return res.json({
|
||||
error: err.message || err,
|
||||
data: []
|
||||
});
|
||||
return handleErrorResponse(res, log, err);
|
||||
}
|
||||
res.status(200);
|
||||
res.json({
|
||||
|
@ -351,11 +255,7 @@ router.get('/subscriptions/:listId', (req, res) => {
|
|||
router.get('/lists', (req, res) => {
|
||||
lists.quicklist((err, lists) => {
|
||||
if (err) {
|
||||
res.status(500);
|
||||
return res.json({
|
||||
error: err.message || err,
|
||||
data: []
|
||||
});
|
||||
return handleErrorResponse(res, log, err);
|
||||
}
|
||||
res.status(200);
|
||||
res.json({
|
||||
|
@ -367,10 +267,7 @@ router.get('/lists', (req, res) => {
|
|||
router.get('/list/:id', (req, res) => {
|
||||
lists.get(req.params.id, (err, list) => {
|
||||
if (err) {
|
||||
res.status(500);
|
||||
return res.json({
|
||||
error: err.message || err,
|
||||
});
|
||||
return handleErrorResponse(res, log, err);
|
||||
}
|
||||
res.status(200);
|
||||
res.json({
|
||||
|
@ -382,11 +279,7 @@ router.get('/list/:id', (req, res) => {
|
|||
router.get('/lists/:email', (req, res) => {
|
||||
lists.getListsWithEmail(req.params.email, (err, lists) => {
|
||||
if (err) {
|
||||
res.status(500);
|
||||
return res.json({
|
||||
error: err.message || err,
|
||||
data: []
|
||||
});
|
||||
return handleErrorResponse(res, log, err);
|
||||
}
|
||||
res.status(200);
|
||||
res.json({
|
||||
|
@ -402,23 +295,15 @@ router.post('/field/:listId', (req, res) => {
|
|||
});
|
||||
lists.getByCid(req.params.listId, (err, list) => {
|
||||
if (err) {
|
||||
log.error('API', err);
|
||||
res.status(500);
|
||||
return res.json({
|
||||
error: err.message || err,
|
||||
data: []
|
||||
});
|
||||
return handleErrorResponse(res, log, err);
|
||||
}
|
||||
if (!list) {
|
||||
res.status(404);
|
||||
return res.json({
|
||||
error: 'Selected listId not found',
|
||||
data: []
|
||||
});
|
||||
return handleErrorResponse(res, log, false, 404, 'Selected listId not found');
|
||||
}
|
||||
|
||||
let field = {
|
||||
name: (input.NAME || '').toString().trim(),
|
||||
description: (input.DESCRIPTION || '').toString().trim(),
|
||||
defaultValue: (input.DEFAULT || '').toString().trim() || null,
|
||||
type: (input.TYPE || '').toString().toLowerCase().trim(),
|
||||
group: Number(input.GROUP) || null,
|
||||
|
@ -428,11 +313,7 @@ router.post('/field/:listId', (req, res) => {
|
|||
|
||||
fields.create(list.id, field, (err, id, tag) => {
|
||||
if (err) {
|
||||
res.status(500);
|
||||
return res.json({
|
||||
error: err.message || err,
|
||||
data: []
|
||||
});
|
||||
return handleErrorResponse(res, log, err);
|
||||
}
|
||||
res.status(200);
|
||||
res.json({
|
||||
|
@ -451,19 +332,11 @@ router.post('/blacklist/add', (req, res) => {
|
|||
input[(key || '').toString().trim().toUpperCase()] = (req.body[key] || '').toString().trim();
|
||||
});
|
||||
if (!(input.EMAIL) || (input.EMAIL === '')) {
|
||||
res.status(500);
|
||||
return res.json({
|
||||
error: 'EMAIL argument are required',
|
||||
data: []
|
||||
});
|
||||
return handleErrorResponse(res, log, err);
|
||||
}
|
||||
blacklist.add(input.EMAIL, (err) =>{
|
||||
if (err) {
|
||||
res.status(500);
|
||||
return res.json({
|
||||
error: err.message || err,
|
||||
data: []
|
||||
});
|
||||
return handleErrorResponse(res, log, err);
|
||||
}
|
||||
res.status(200);
|
||||
res.json({
|
||||
|
@ -478,19 +351,11 @@ router.post('/blacklist/delete', (req, res) => {
|
|||
input[(key || '').toString().trim().toUpperCase()] = (req.body[key] || '').toString().trim();
|
||||
});
|
||||
if (!(input.EMAIL) || (input.EMAIL === '')) {
|
||||
res.status(500);
|
||||
return res.json({
|
||||
error: 'EMAIL argument are required',
|
||||
data: []
|
||||
});
|
||||
return handleErrorResponse(res, log, false, 500, 'EMAIL argument are required');
|
||||
}
|
||||
blacklist.delete(input.EMAIL, (err) =>{
|
||||
if (err) {
|
||||
res.status(500);
|
||||
return res.json({
|
||||
error: err.message || err,
|
||||
data: []
|
||||
});
|
||||
return handleErrorResponse(res, log, err);
|
||||
}
|
||||
res.status(200);
|
||||
res.json({
|
||||
|
@ -506,11 +371,7 @@ router.get('/blacklist/get', (req, res) => {
|
|||
|
||||
blacklist.get(start, limit, search, (err, data, total) => {
|
||||
if (err) {
|
||||
res.status(500);
|
||||
return res.json({
|
||||
error: err.message || err,
|
||||
data: []
|
||||
});
|
||||
return handleErrorResponse(res, log, err);
|
||||
}
|
||||
res.status(200);
|
||||
res.json({
|
||||
|
@ -524,4 +385,66 @@ router.get('/blacklist/get', (req, res) => {
|
|||
});
|
||||
});
|
||||
|
||||
router.post('/changeemail/:listId', (req, res) => {
|
||||
let input = {};
|
||||
Object.keys(req.body).forEach(key => {
|
||||
input[(key || '').toString().trim().toUpperCase()] = (req.body[key] || '').toString().trim();
|
||||
});
|
||||
if (!(input.EMAILOLD) || (input.EMAILOLD === '')) {
|
||||
return handleErrorResponse(res, log, false, 500, 'EMAILOLD argument is required');
|
||||
}
|
||||
if (!(input.EMAILNEW) || (input.EMAILNEW === '')) {
|
||||
return handleErrorResponse(res, log, false, 500, 'EMAILNEW argument is required');
|
||||
}
|
||||
lists.getByCid(req.params.listId, (err, list) => {
|
||||
if (err) {
|
||||
return handleErrorResponse(res, log, err);
|
||||
}
|
||||
if (!list) {
|
||||
return handleErrorResponse(res, log, false, 404, 'Selected listId not found');
|
||||
}
|
||||
blacklist.isblacklisted(input.EMAILNEW, (err, blacklisted) => {
|
||||
if (err) {
|
||||
return handleErrorResponse(res, log, err);
|
||||
}
|
||||
if (blacklisted) {
|
||||
return handleErrorResponse(res, log, false, 500, 'New email is blacklisted');
|
||||
}
|
||||
|
||||
subscriptions.getByEmail(list.id, input.EMAILOLD, (err, subscription) => {
|
||||
if (err) {
|
||||
return handleErrorResponse(res, log, err);
|
||||
}
|
||||
|
||||
if (!subscription) {
|
||||
return handleErrorResponse(res, log, false, 404, 'Subscription with given old email not found');
|
||||
}
|
||||
|
||||
subscriptions.updateAddressCheck(list, subscription.cid, input.EMAILNEW, null, (err, old, valid) => {
|
||||
if (err) {
|
||||
return handleErrorResponse(res, log, err);
|
||||
}
|
||||
|
||||
if (!valid) {
|
||||
return handleErrorResponse(res, log, false, 500, 'New email not valid');
|
||||
}
|
||||
|
||||
subscriptions.updateAddress(list.id, subscription.id, input.EMAILNEW, (err) => {
|
||||
if (err) {
|
||||
return handleErrorResponse(res, log, err);
|
||||
}
|
||||
res.status(200);
|
||||
res.json({
|
||||
data: {
|
||||
id: subscription.id,
|
||||
changedemail: true
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
|
|
@ -126,6 +126,19 @@ router.post('/create', passport.parseForm, passport.csrfProtection, (req, res) =
|
|||
});
|
||||
});
|
||||
|
||||
router.post('/duplicate', passport.parseForm, passport.csrfProtection, (req, res) => {
|
||||
campaigns.duplicate(req.body.id, (err, duplicated) => {
|
||||
if (err) {
|
||||
req.flash('danger', err && err.message || err);
|
||||
} else if (duplicated) {
|
||||
req.flash('success', _('Campaign duplicated'));
|
||||
} else {
|
||||
req.flash('info', _('Could not duplicate specified campaign'));
|
||||
}
|
||||
return res.redirect('/campaigns/edit/' + duplicated);
|
||||
});
|
||||
});
|
||||
|
||||
router.get('/edit/:id', passport.csrfProtection, (req, res, next) => {
|
||||
campaigns.get(req.params.id, false, (err, campaign) => {
|
||||
if (err || !campaign) {
|
||||
|
|
11
setup/sql/upgrade-00034.sql
Normal file
11
setup/sql/upgrade-00034.sql
Normal file
|
@ -0,0 +1,11 @@
|
|||
# Header section
|
||||
# Define incrementing schema version number
|
||||
SET @schema_version = '34';
|
||||
|
||||
# Add template field for group elements
|
||||
ALTER TABLE `custom_fields` ADD COLUMN `description` text AFTER `name`;
|
||||
|
||||
# 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;
|
|
@ -17,6 +17,11 @@
|
|||
<input type="hidden" name="id" value="{{id}}" />
|
||||
</form>
|
||||
|
||||
<form method="post" class="duplicate-form" id="campaigns-duplicate" action="/campaigns/duplicate">
|
||||
<input type="hidden" name="_csrf" value="{{csrfToken}}">
|
||||
<input type="hidden" name="id" value="{{id}}" />
|
||||
</form>
|
||||
|
||||
{{#each attachments}}
|
||||
<form method="post" id="attachment-download-{{id}}" action="/campaigns/attachment/download">
|
||||
<input type="hidden" name="_csrf" value="{{../csrfToken}}">
|
||||
|
@ -240,6 +245,7 @@
|
|||
<div class="form-group">
|
||||
<div class="col-sm-offset-2 col-sm-10">
|
||||
<div class="pull-right">
|
||||
<button type="submit" form="campaigns-duplicate" class="btn btn-default"> {{#translate}}Duplicate{{/translate}}</button>
|
||||
<button type="submit" form="campaigns-delete" class="btn btn-danger"><i class="glyphicon glyphicon-remove"></i> {{#translate}}Delete Campaign{{/translate}}</button>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary"><i class="glyphicon glyphicon-ok"></i> {{#translate}}Update{{/translate}}</button>
|
||||
|
|
|
@ -19,6 +19,13 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="description" class="col-sm-2 control-label">{{#translate}}Field Description{{/translate}}</label>
|
||||
<div class="col-sm-10">
|
||||
<textarea class="form-control gpg-text" rows="3" name="description" id="description">{{field.description}}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="type" class="col-sm-2 control-label">{{#translate}}Field Type{{/translate}}</label>
|
||||
<div class="col-sm-10">
|
||||
|
|
|
@ -26,6 +26,13 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="description" class="col-sm-2 control-label">{{#translate}}Field Description{{/translate}}</label>
|
||||
<div class="col-sm-10">
|
||||
<textarea class="form-control gpg-text" rows="3" name="description" id="description">{{field.description}}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="type" class="col-sm-2 control-label">{{#translate}}Field Type{{/translate}}</label>
|
||||
<div class="col-sm-10">
|
||||
|
|
|
@ -36,7 +36,7 @@
|
|||
{{#translate}}Equals{{/translate}}
|
||||
</option>
|
||||
<option value="1" {{#if value.negate}} selected {{/if}}>
|
||||
{{#translate}}Not squals{{/translate}}
|
||||
{{#translate}}Not equals{{/translate}}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
|
|
@ -111,7 +111,7 @@
|
|||
$.post('/editorapi/update?type={{type}}&editor={{resource.editorName}}', {
|
||||
id: {{resource.id}},
|
||||
name: '{{resource.name}}',
|
||||
{{#if resource.list}} list: {{resource.list}}, {{/if}}
|
||||
{{#if resource.list}} list: {{resource.list}}+':'+{{resource.segment}}, {{/if}}
|
||||
html: html,
|
||||
editorData: JSON.stringify({
|
||||
template: '{{resource.editorData.template}}',
|
||||
|
|
|
@ -134,9 +134,12 @@
|
|||
<label>{{name}}</label>
|
||||
<input type="hidden" name="origin_{{key}}" value="webform">
|
||||
{{#each options}}
|
||||
<p>
|
||||
<label class="label-checkbox">
|
||||
<input type="checkbox" name="{{column}}" value="1" {{#if value}} checked {{/if}}> {{name}}
|
||||
</label>
|
||||
<span class="help-block">{{description}}</span>
|
||||
</p>
|
||||
{{/each}}
|
||||
</div>
|
||||
{{/if}}
|
||||
|
|
|
@ -181,6 +181,7 @@
|
|||
</p>
|
||||
<ul>
|
||||
<li><strong>NAME</strong> – {{#translate}}field name{{/translate}} (<em>{{#translate}}required{{/translate}}</em>)</li>
|
||||
<li><strong>DESCRIPTION</strong> – {{#translate}}field description{{/translate}}</li>
|
||||
<li><strong>TYPE</strong> – {{#translate}}one of the following types:{{/translate}}
|
||||
<ul>
|
||||
{{#each allowedTypes}}
|
||||
|
@ -334,3 +335,32 @@
|
|||
</p>
|
||||
|
||||
<pre>curl -XGET '{{serviceUrl}}api/list/1?access_token={{accessToken}}'</pre>
|
||||
|
||||
<h3>POST /api/changeemail/:listId – {{#translate}}Change email of existing list subscriber{{/translate}}</h3>
|
||||
|
||||
<p>
|
||||
{{#translate}}This API call changes the email address of an existing list subscriber.{{/translate}}
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<strong>GET</strong> {{#translate}}arguments{{/translate}}
|
||||
</p>
|
||||
<ul>
|
||||
<li><strong>access_token</strong> – {{#translate}}your personal access token{{/translate}}
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
<strong>POST</strong> {{#translate}}arguments{{/translate}}
|
||||
</p>
|
||||
<ul>
|
||||
<li><strong>EMAILOLD</strong> – {{#translate}}subscriber's old email address{{/translate}} (<em>{{#translate}}required{{/translate}}</em>)
|
||||
<li><strong>EMAILNEW</strong> – {{#translate}}subscriber's new email address{{/translate}} (<em>{{#translate}}required{{/translate}}</em>)
|
||||
</ul>
|
||||
|
||||
<p>
|
||||
<strong>{{#translate}}Example{{/translate}}</strong>
|
||||
</p>
|
||||
|
||||
<pre>curl -XPOST {{serviceUrl}}api/changeemail/B16uVTdW?access_token={{accessToken}} \
|
||||
--data 'EMAILOLD=test@example.com&EMAILNEW=foo@bar.com'</pre>
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue