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
|
npm-debug.log
|
||||||
package-lock.json
|
package-lock.json
|
||||||
.DS_Store
|
.DS_Store
|
||||||
config/development.*
|
config/development*
|
||||||
config/production.*
|
config/production*
|
||||||
config/test.*
|
config/test*
|
||||||
workers/reports/config/development.*
|
workers/reports/config/development.*
|
||||||
workers/reports/config/production.*
|
workers/reports/config/production.*
|
||||||
workers/reports/config/test.*
|
workers/reports/config/test.*
|
||||||
|
|
54
Dockerfile
54
Dockerfile
|
@ -1,13 +1,47 @@
|
||||||
FROM node:8.6
|
# Mutistaged Node.js Build
|
||||||
|
FROM node:10-alpine as builder
|
||||||
|
|
||||||
# First install dependencies
|
# Install system dependencies
|
||||||
COPY ./package.json ./app/
|
RUN set -ex; \
|
||||||
WORKDIR /app/
|
apk add --update --no-cache \
|
||||||
ENV NODE_ENV production
|
make gcc g++ git python
|
||||||
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
|
# 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
|
||||||
|
|
||||||
|
# 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
|
# to download and install all the NPM dependencies every time there's a change in the source code
|
||||||
COPY . /app
|
COPY . /app
|
||||||
EXPOSE 3000
|
|
||||||
ENTRYPOINT ["bash", "/app/docker-entrypoint.sh"]
|
RUN set -ex; \
|
||||||
CMD ["node", "index.js"]
|
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"]
|
|
@ -1,3 +1,5 @@
|
||||||
|
## Mailtrain v2 beta is available. Check it out here https://github.com/Mailtrain-org/mailtrain/tree/development
|
||||||
|
|
||||||
# Mailtrain
|
# Mailtrain
|
||||||
|
|
||||||
[Mailtrain](http://mailtrain.org) is a self hosted newsletter application built on Node.js (v7+) and MySQL (v5.5+ or MariaDB).
|
[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`
|
* 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).
|
* 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`
|
* 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.
|
* 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)
|
## 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`
|
4. Run the server `NODE_ENV=production npm start`
|
||||||
5. Open [http://localhost:3000/](http://localhost:3000/)
|
5. Open [http://localhost:3000/](http://localhost:3000/)
|
||||||
6. Authenticate as `admin`:`test`
|
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
|
8. Navigate to [http://localhost:3000/users/account](http://localhost:3000/users/account) and update user information and password
|
||||||
|
|
||||||
## Read The Docs
|
## 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).
|
[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
|
## 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))
|
* Almost no documentation (there are some guides in the [Wiki](https://github.com/Mailtrain-org/mailtrain/wiki))
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
@ -76,13 +76,13 @@ Install script installs and sets up the following:
|
||||||
* **logrotate** to rotate Mailtrain log files
|
* **logrotate** to rotate Mailtrain log files
|
||||||
* **upstart** or **systemd** init script to automatically start and manage Mailtrain process
|
* **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
|
#### Next steps after installation
|
||||||
|
|
||||||
##### 1. Change admin password
|
##### 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**
|
* Username: **admin**
|
||||||
* Password: **test**
|
* 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
|
##### 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
|
##### 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
|
##### 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.
|
> 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:
|
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
|
```sh
|
||||||
mkdir /opt/dkim-keys
|
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
|
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];"
|
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)
|
- 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
|
- 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.
|
- 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.
|
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:
|
#### 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`
|
* 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.
|
* 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.
|
* 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`.
|
* 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`.
|
||||||
|
@ -197,7 +197,7 @@ If you run Mailtrain on a VPS, you will have to find the PTR configuration somew
|
||||||
|
|
||||||
## Upgrade
|
## Upgrade
|
||||||
|
|
||||||
* Replace old files with new ones by running in the Mailtrain folder `git pull origin master` if you used Git to set Mailtrain up or just download [new files](https://github.com/Mailtrain-org/mailtrain/archive/master.zip) and replace old ones with these
|
* Replace old files with new ones by running in the Mailtrain folder `git pull origin master` if you used Git to set Mailtrain up or just download [new files](https://github.com/Mailtrain-org/mailtrain/archive/master.zip) and replace old ones with these
|
||||||
* Run `npm install --production` in the Mailtrain folder
|
* Run `npm install --production` in the Mailtrain folder
|
||||||
|
|
||||||
## Using Environment Variables
|
## Using Environment Variables
|
||||||
|
@ -221,11 +221,11 @@ Mailtrain uses [node-config](https://github.com/lorenwest/node-config) for confi
|
||||||
|
|
||||||
### Running Behind Nginx Proxy
|
### 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
|
### 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
|
## Subscription Widget
|
||||||
|
|
||||||
|
@ -273,7 +273,7 @@ If using VERP with iRedMail, see [this post](http://www.iredmail.org/forum/post4
|
||||||
|
|
||||||
## Testing
|
## 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:
|
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
|
## 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
|
```toml
|
||||||
language="zz"
|
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 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.
|
*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.
|
> **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
|
#: views/blacklist.hbs:4 views/users/api.hbs:55
|
||||||
msgid "Add email to blacklist"
|
msgid "Add email to blacklist"
|
||||||
msgstr "E-Mail zur Blacklist hinzufügen"
|
msgstr "E-Mail Adresse zur Blacklist hinzufügen"
|
||||||
|
|
||||||
#: views/blacklist.hbs:5
|
#: views/blacklist.hbs:5
|
||||||
msgid "Add"
|
msgid "Add"
|
||||||
|
@ -151,7 +151,7 @@ msgstr "SMTP Antwort"
|
||||||
|
|
||||||
#: views/campaigns/bounced.hbs:11
|
#: views/campaigns/bounced.hbs:11
|
||||||
msgid "Bounce time"
|
msgid "Bounce time"
|
||||||
msgstr "Bouncen Zeitpunkt"
|
msgstr "Bounce Zeitpunkt"
|
||||||
|
|
||||||
#: views/campaigns/campaigns.hbs:3 views/campaigns/create-triggered.hbs:25
|
#: views/campaigns/campaigns.hbs:3 views/campaigns/create-triggered.hbs:25
|
||||||
#: views/campaigns/create.hbs:3 views/campaigns/create.hbs:4
|
#: 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 "
|
"new entry is found from this feed it is sent to selected list as an email "
|
||||||
"message."
|
"message."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"RSS-Kampagne setzt einen Tracker gegen den gewählten RSS-Feed. Wenn ein "
|
"Eine RSS-Kampagne setzt einen Tracker auf den gewählten RSS-Feed. Wenn ein "
|
||||||
"neuer Eintrag aus diesem Feed gefunden wird, wird er als E-Mail an die "
|
"neuer Eintrag in diesem Feed gefunden wird, wird er als E-Mail an die "
|
||||||
"ausgewählte Liste gesendet."
|
"ausgewählte Liste gesendet."
|
||||||
|
|
||||||
#: views/campaigns/create-rss.hbs:7 views/campaigns/create-triggered.hbs:6
|
#: 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/edit-triggered.hbs:16 views/campaigns/edit.hbs:17
|
||||||
#: views/campaigns/view.hbs:12
|
#: views/campaigns/view.hbs:12
|
||||||
msgid "Email \"from name\""
|
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-rss.hbs:16 views/campaigns/create-triggered.hbs:19
|
||||||
#: views/campaigns/create.hbs:19 views/campaigns/edit-rss.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/campaigns/edit-triggered.hbs:17 views/campaigns/edit.hbs:18
|
||||||
#: views/settings.hbs:23
|
#: views/settings.hbs:23
|
||||||
msgid "This is the name your emails will come from"
|
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-rss.hbs:17 views/campaigns/create-triggered.hbs:20
|
||||||
#: views/campaigns/create.hbs:20 views/campaigns/edit-rss.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/edit-triggered.hbs:18 views/campaigns/edit.hbs:19
|
||||||
#: views/campaigns/view.hbs:13
|
#: views/campaigns/view.hbs:13
|
||||||
msgid "Email \"from\" address"
|
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/create-rss.hbs:18 views/campaigns/create-triggered.hbs:21
|
||||||
#: views/campaigns/edit-rss.hbs:21 views/campaigns/edit-triggered.hbs:19
|
#: 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"
|
msgstr "Dies ist die Reply-To Adresse"
|
||||||
|
|
||||||
#: views/campaigns/create-rss.hbs:19 views/campaigns/create-triggered.hbs:24
|
#: 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/create.hbs:28 views/campaigns/edit-rss.hbs:22
|
||||||
#: views/campaigns/edit-triggered.hbs:22 views/campaigns/edit.hbs:25
|
#: views/campaigns/edit-triggered.hbs:22 views/campaigns/edit.hbs:27
|
||||||
msgid "Disable clicked/opened tracking"
|
msgid "Disable opened tracking"
|
||||||
msgstr "Tracking deaktivieren"
|
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:3
|
||||||
#: views/campaigns/create-triggered.hbs:4
|
#: 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
|
#: views/campaigns/create-triggered.hbs:15 views/campaigns/create.hbs:15
|
||||||
msgid "Selecting a template creates a campaign specific copy from it"
|
msgid "Selecting a template creates a campaign specific copy from it"
|
||||||
msgstr ""
|
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
|
#: views/campaigns/create-triggered.hbs:16 views/campaigns/create.hbs:16
|
||||||
msgid "Or alternatively use an URL as the message content source:"
|
msgid "Or alternatively use an URL as the message content source:"
|
||||||
msgstr ""
|
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/create-triggered.hbs:17 views/campaigns/create.hbs:17
|
||||||
#: views/campaigns/edit-triggered.hbs:25 views/campaigns/edit.hbs:28
|
#: 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/edit-triggered.hbs:20 views/campaigns/edit.hbs:23
|
||||||
#: views/campaigns/view.hbs:15
|
#: views/campaigns/view.hbs:15
|
||||||
msgid "Email \"subject line\""
|
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/create-triggered.hbs:23 views/campaigns/create.hbs:25
|
||||||
#: views/campaigns/edit-triggered.hbs:21 views/campaigns/edit.hbs:24
|
#: views/campaigns/edit-triggered.hbs:21 views/campaigns/edit.hbs:24
|
||||||
#: views/settings.hbs:27
|
#: views/settings.hbs:27
|
||||||
msgid "Keep it relevant and non-spammy"
|
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
|
#: views/campaigns/create.hbs:21 views/campaigns/edit.hbs:20
|
||||||
msgid ""
|
msgid ""
|
||||||
|
@ -445,7 +451,7 @@ msgid ""
|
||||||
"set"
|
"set"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Dies ist die Adresse, an welche Antworten gesendet werden, ausser die "
|
"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/create.hbs:22 views/campaigns/edit.hbs:21
|
||||||
#: views/campaigns/view.hbs:14
|
#: views/campaigns/view.hbs:14
|
||||||
|
@ -458,15 +464,23 @@ msgstr ""
|
||||||
"Falls gesetzt, ist dies die Adresse an welche E-Mail-Antworten gesendet "
|
"Falls gesetzt, ist dies die Adresse an welche E-Mail-Antworten gesendet "
|
||||||
"werden"
|
"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
|
#: views/campaigns/delivered.hbs:3 views/campaigns/delivered.hbs:4
|
||||||
msgid "Delivered info"
|
msgid "Delivered info"
|
||||||
msgstr "Zustellungs-Info"
|
msgstr "Zustellungs Info"
|
||||||
|
|
||||||
#: views/campaigns/delivered.hbs:6
|
#: views/campaigns/delivered.hbs:6
|
||||||
msgid "Subscribers who received the message and did not bounce/unsubscribe:"
|
msgid "Subscribers who received the message and did not bounce/unsubscribe:"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Abonnenten, welche die Nachricht erhalten haben und nicht bounced oder "
|
"Abonnenten, welche die Nachricht erfolgreich erhalten (kein \"bounce\") und "
|
||||||
"abbestellen:"
|
"nicht deabonniert haben:"
|
||||||
|
|
||||||
#: views/campaigns/delivered.hbs:11
|
#: views/campaigns/delivered.hbs:11
|
||||||
msgid "Delivery time"
|
msgid "Delivery time"
|
||||||
|
@ -516,7 +530,7 @@ msgstr "Generell"
|
||||||
|
|
||||||
#: views/campaigns/edit-triggered.hbs:23 views/campaigns/edit.hbs:26
|
#: views/campaigns/edit-triggered.hbs:23 views/campaigns/edit.hbs:26
|
||||||
msgid "Template Settings"
|
msgid "Template Settings"
|
||||||
msgstr "Vorlagen-Einstellungen"
|
msgstr "Vorlagen Einstellungen"
|
||||||
|
|
||||||
#: views/campaigns/edit-triggered.hbs:24 views/campaigns/edit.hbs:27
|
#: views/campaigns/edit-triggered.hbs:24 views/campaigns/edit.hbs:27
|
||||||
msgid "Template URL"
|
msgid "Template URL"
|
||||||
|
@ -568,7 +582,7 @@ msgstr "Anzahl Öffnungen"
|
||||||
|
|
||||||
#: views/campaigns/unsubscribed.hbs:3 views/campaigns/unsubscribed.hbs:4
|
#: views/campaigns/unsubscribed.hbs:3 views/campaigns/unsubscribed.hbs:4
|
||||||
msgid "Unsubscribed info"
|
msgid "Unsubscribed info"
|
||||||
msgstr "Abmeldungs-Info"
|
msgstr "Deabonnement Info"
|
||||||
|
|
||||||
#: views/campaigns/unsubscribed.hbs:6
|
#: views/campaigns/unsubscribed.hbs:6
|
||||||
msgid "Subscribers who unsubscribed:"
|
msgid "Subscribers who unsubscribed:"
|
||||||
|
@ -577,7 +591,7 @@ msgstr "Abonnenten welche deabonnierten:"
|
||||||
#: views/campaigns/unsubscribed.hbs:11 views/campaigns/view.hbs:28
|
#: views/campaigns/unsubscribed.hbs:11 views/campaigns/view.hbs:28
|
||||||
#: views/lists/subscription/import.hbs:10 routes/lists.js:202
|
#: views/lists/subscription/import.hbs:10 routes/lists.js:202
|
||||||
msgid "Unsubscribed"
|
msgid "Unsubscribed"
|
||||||
msgstr "Abbestellt"
|
msgstr "Deabonniert"
|
||||||
|
|
||||||
#: views/campaigns/upload-attachment.hbs:7
|
#: views/campaigns/upload-attachment.hbs:7
|
||||||
msgid "Upload"
|
msgid "Upload"
|
||||||
|
@ -619,7 +633,7 @@ msgstr "Kampagnen Vorschau als"
|
||||||
|
|
||||||
#: views/campaigns/view.hbs:17
|
#: views/campaigns/view.hbs:17
|
||||||
msgid "Add new test user"
|
msgid "Add new test user"
|
||||||
msgstr "Neuen Test-User hinzufügen"
|
msgstr "Neuen Testbenutzer hinzufügen"
|
||||||
|
|
||||||
#: views/campaigns/view.hbs:18
|
#: views/campaigns/view.hbs:18
|
||||||
msgid "No test users yet, create one here"
|
msgid "No test users yet, create one here"
|
||||||
|
@ -643,7 +657,7 @@ msgstr "Blacklisted"
|
||||||
|
|
||||||
#: views/campaigns/view.hbs:23
|
#: views/campaigns/view.hbs:23
|
||||||
msgid "List subscribers who blacklisted by global blacklist"
|
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
|
#: views/campaigns/view.hbs:24 routes/lists.js:202
|
||||||
msgid "Bounced"
|
msgid "Bounced"
|
||||||
|
@ -685,7 +699,7 @@ msgstr "Abonnenten, die auf einen Link geklickt haben"
|
||||||
msgid ""
|
msgid ""
|
||||||
"Are you sure? This action would start sending messages to the selected list"
|
"Are you sure? This action would start sending messages to the selected list"
|
||||||
msgstr ""
|
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"
|
"ausgewählte Liste beginnen"
|
||||||
|
|
||||||
#: views/campaigns/view.hbs:35
|
#: views/campaigns/view.hbs:35
|
||||||
|
@ -706,7 +720,7 @@ msgstr "An Abonnenten senden:"
|
||||||
|
|
||||||
#: views/campaigns/view.hbs:39
|
#: views/campaigns/view.hbs:39
|
||||||
msgid "Are you sure? This action would reset scheduling"
|
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
|
#: views/campaigns/view.hbs:40
|
||||||
msgid "Cancel"
|
msgid "Cancel"
|
||||||
|
@ -728,7 +742,7 @@ msgstr "Am senden"
|
||||||
msgid ""
|
msgid ""
|
||||||
"Are you sure? This action would resume sending messages to the selected list"
|
"Are you sure? This action would resume sending messages to the selected list"
|
||||||
msgstr ""
|
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"
|
"ausgewählte Liste fortsetzen"
|
||||||
|
|
||||||
#: views/campaigns/view.hbs:45 views/campaigns/view.hbs:49
|
#: 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 "
|
"Are you sure? This action would pause sending new entries in RSS feed as "
|
||||||
"email messages to the selected list"
|
"email messages to the selected list"
|
||||||
msgstr ""
|
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"
|
"als E-Mail-Nachrichten an die ausgewählte Liste pausieren"
|
||||||
|
|
||||||
#: views/campaigns/view.hbs:55 views/campaigns/view.hbs:59
|
#: 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
|
#: views/emails/password-reset-text.hbs:2
|
||||||
msgid "We have received a password change request for your Mailtrain account:"
|
msgid "We have received a password change request for your Mailtrain account:"
|
||||||
msgstr ""
|
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-html.hbs:3
|
||||||
#: views/emails/password-reset-text.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 "
|
"If you did not ask to change your password, then you can ignore this email "
|
||||||
"and your password will not be changed."
|
"and your password will not be changed."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Wenn Sie nicht angefordert haben, Ihr Passwort zu ändern, können Sie diese E-"
|
"Wenn Sie Ihr Passwort nicht ändern wollen, können Sie diese E-"
|
||||||
"Mail ignorieren und Ihr Passwort wird nicht geändert."
|
"Mail einfach ignorieren."
|
||||||
|
|
||||||
#: views/emails/rss-html.hbs:1 views/emails/stationery-html.hbs:3
|
#: views/emails/rss-html.hbs:1 views/emails/stationery-html.hbs:3
|
||||||
#: views/emails/stationery-text.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/rss-html.hbs:3 views/emails/stationery-html.hbs:5
|
||||||
#: views/emails/stationery-text.hbs:5
|
#: views/emails/stationery-text.hbs:5
|
||||||
msgid "View this email in your browser"
|
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
|
#: views/emails/stationery-html.hbs:1 views/emails/stationery-text.hbs:1
|
||||||
msgid "Hey [FIRST_NAME/Customer],"
|
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 "
|
"API or import from a CSV file. All lists come with support for custom fields "
|
||||||
"and merge tags as well."
|
"and merge tags as well."
|
||||||
msgstr ""
|
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 "
|
"Abonnenten? Kein Problem. Sie können Abonnenten manuell über die API "
|
||||||
"hinzufügen oder aus einer CSV-Datei importieren. Alle Listen kommen mit "
|
"hinzufügen oder aus einer CSV-Datei importieren. Für alle Listen können "
|
||||||
"Unterstützung für benutzerdefinierte Felder und Merge-Tags."
|
"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: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
|
#: 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/edit.hbs:3 views/lists/fields/fields.hbs:3
|
||||||
#: views/lists/fields/fields.hbs:5 views/lists/view.hbs:6
|
#: views/lists/fields/fields.hbs:5 views/lists/view.hbs:6
|
||||||
msgid "Custom Fields"
|
msgid "Custom Fields"
|
||||||
msgstr "Felder"
|
msgstr "Zusätzliche Datenfelder"
|
||||||
|
|
||||||
#: views/index.hbs:5
|
#: views/index.hbs:5
|
||||||
msgid ""
|
msgid ""
|
||||||
|
@ -908,8 +922,8 @@ msgid ""
|
||||||
"Every custom field can be included in the generated newsletters through "
|
"Every custom field can be included in the generated newsletters through "
|
||||||
"merge tags."
|
"merge tags."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Textfelder, Nummern, Dropdowns oder Checkboxen, Mailtrain hat sie alle. "
|
"Textfelder, Nummern, Dropdowns oder Checkboxen, Mailtrain bietet all das. "
|
||||||
"Jedes benutzerdefinierte Feld kann in den generierten Newslettern mittels "
|
"Jedes zusätzliche Feld kann in den generierten Newslettern über "
|
||||||
"Merge-Tags eingebunden werden."
|
"Merge-Tags eingebunden werden."
|
||||||
|
|
||||||
#: views/index.hbs:6
|
#: views/index.hbs:6
|
||||||
|
@ -918,7 +932,7 @@ msgstr "Mailtrain bietet auch benutzerdefinierte Formulare."
|
||||||
|
|
||||||
#: views/index.hbs:8
|
#: views/index.hbs:8
|
||||||
msgid "List Segmentation"
|
msgid "List Segmentation"
|
||||||
msgstr "Segmentierung"
|
msgstr "Listen Segmentierung"
|
||||||
|
|
||||||
#: views/index.hbs:9
|
#: views/index.hbs:9
|
||||||
msgid ""
|
msgid ""
|
||||||
|
@ -939,13 +953,13 @@ msgid ""
|
||||||
"then Mailtrain auto-generates a new campaign using entry data as message "
|
"then Mailtrain auto-generates a new campaign using entry data as message "
|
||||||
"contents and sends it to selected subscribers."
|
"contents and sends it to selected subscribers."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Mailtrain kann RSS-Feeds verfolgen, und wenn ein neuer Eintrag in einem Feed "
|
"Mailtrain kann RSS-Feeds verfolgen und bei neuen Einträgen automatisch "
|
||||||
"erkannt wird, sendet Mailtrain automatisch eine neue Kampagne mit dem RSS-"
|
"eine neue Kampagne mit dem RSS-Beitrag als Nachrichteninhalt an die "
|
||||||
"Beitrag als Nachrichteninhalt an die ausgewählte Liste."
|
"ausgewählte Liste senden."
|
||||||
|
|
||||||
#: views/index.hbs:14
|
#: views/index.hbs:14
|
||||||
msgid "GPG Encryption"
|
msgid "GPG Encryption"
|
||||||
msgstr "GPG-Verschlüsselung"
|
msgstr "GPG Verschlüsselung"
|
||||||
|
|
||||||
#: views/index.hbs:15
|
#: views/index.hbs:15
|
||||||
msgid ""
|
msgid ""
|
||||||
|
@ -954,7 +968,7 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Wenn für eine Liste ein benutzerdefiniertes Feld für den GPG-Public-Key "
|
"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 "
|
"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
|
#: views/index.hbs:17
|
||||||
msgid "Click Stats"
|
msgid "Click Stats"
|
||||||
|
@ -984,7 +998,7 @@ msgstr ""
|
||||||
|
|
||||||
#: views/index.hbs:23
|
#: views/index.hbs:23
|
||||||
msgid "Send via Any Provider"
|
msgid "Send via Any Provider"
|
||||||
msgstr "Sende mit"
|
msgstr "Sende mit beliebigen Anbietern"
|
||||||
|
|
||||||
#: views/index.hbs:24
|
#: views/index.hbs:24
|
||||||
msgid ""
|
msgid ""
|
||||||
|
@ -1015,7 +1029,7 @@ msgstr ""
|
||||||
|
|
||||||
#: views/index.hbs:29
|
#: views/index.hbs:29
|
||||||
msgid "Donate to Author"
|
msgid "Donate to Author"
|
||||||
msgstr "Dem Autor Spenden"
|
msgstr "Dem Autor spenden"
|
||||||
|
|
||||||
#: views/index.hbs:30
|
#: views/index.hbs:30
|
||||||
msgid "Mailtrain is available under GPLv3 license and completely open source."
|
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"
|
msgid "Allow public users to subscribe themselves"
|
||||||
msgstr "Allen erlauben, diese Liste selbst zu abonnieren"
|
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
|
#: views/lists/edit.hbs:3 views/lists/edit.hbs:4 views/lists/view.hbs:8
|
||||||
msgid "Edit List"
|
msgid "Edit List"
|
||||||
msgstr "Liste bearbeiten"
|
msgstr "Liste bearbeiten"
|
||||||
|
@ -1145,7 +1168,7 @@ msgstr "Diese Listen ID wird den Abonnenten dargestellt"
|
||||||
|
|
||||||
#: views/lists/edit.hbs:12
|
#: views/lists/edit.hbs:12
|
||||||
msgid "Custom Form"
|
msgid "Custom Form"
|
||||||
msgstr "Formular"
|
msgstr "Benutzerdefiniertes Formular"
|
||||||
|
|
||||||
#: views/lists/edit.hbs:13 views/lists/forms/forms.hbs:11
|
#: views/lists/edit.hbs:13 views/lists/forms/forms.hbs:11
|
||||||
msgid "Default Mailtrain Form"
|
msgid "Default Mailtrain Form"
|
||||||
|
@ -1153,11 +1176,18 @@ msgstr "Standard Mailtrain Formular"
|
||||||
|
|
||||||
#: views/lists/edit.hbs:14
|
#: views/lists/edit.hbs:14
|
||||||
msgid ""
|
msgid ""
|
||||||
"The custom form used for this list. You can create a form <a href=\"/forms/"
|
"The custom form used for this list. You can create a form <a href=\"/forms/{{id}}/create\">here</a>."
|
||||||
"{{id}}/create\">here</a>."
|
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Das Standard-Formular dieser Liste. Wenn Sie ein Formular erstellt möchten, "
|
"Das Standard-Formular dieser Liste. Wenn Sie ein benutzerdefiniertes Formular "
|
||||||
"klicken Sie <a href=\"/forms/{{id}}/create\">hier</a>."
|
"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
|
#: views/lists/edit.hbs:16
|
||||||
msgid "Delete List"
|
msgid "Delete List"
|
||||||
|
@ -1169,7 +1199,7 @@ msgstr "Feld erstellen"
|
||||||
|
|
||||||
#: views/lists/fields/create.hbs:5 views/lists/fields/fields.hbs:4
|
#: views/lists/fields/create.hbs:5 views/lists/fields/fields.hbs:4
|
||||||
msgid "Create Custom Field"
|
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/create.hbs:6 views/lists/fields/create.hbs:7
|
||||||
#: views/lists/fields/edit.hbs:7 views/lists/fields/edit.hbs:8
|
#: 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
|
#: views/lists/fields/create.hbs:23 views/lists/fields/edit.hbs:24
|
||||||
#: lib/models/fields.js:22
|
#: lib/models/fields.js:22
|
||||||
msgid "Radio Buttons"
|
msgid "Radio Buttons"
|
||||||
msgstr "Radio Knöpfe"
|
msgstr "Radio Buttons"
|
||||||
|
|
||||||
#: views/lists/fields/create.hbs:24 views/lists/fields/edit.hbs:25
|
#: views/lists/fields/create.hbs:24 views/lists/fields/edit.hbs:25
|
||||||
#: lib/models/fields.js:23
|
#: 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/edit.hbs:35 views/lists/fields/edit.hbs:36
|
||||||
#: views/lists/fields/fields.hbs:9
|
#: views/lists/fields/fields.hbs:9
|
||||||
msgid "Default merge tag value"
|
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
|
#: views/lists/fields/create.hbs:32 views/lists/fields/edit.hbs:34
|
||||||
msgid ""
|
msgid ""
|
||||||
|
@ -1305,7 +1335,7 @@ msgstr "Feld bearbeiten"
|
||||||
|
|
||||||
#: views/lists/fields/edit.hbs:5
|
#: views/lists/fields/edit.hbs:5
|
||||||
msgid "Edit Custom Field"
|
msgid "Edit Custom Field"
|
||||||
msgstr "Feld bearbeiten"
|
msgstr "Zusätzliches Feld bearbeiten"
|
||||||
|
|
||||||
#: views/lists/fields/edit.hbs:6
|
#: views/lists/fields/edit.hbs:6
|
||||||
msgid "Back to fields"
|
msgid "Back to fields"
|
||||||
|
@ -1347,7 +1377,7 @@ msgstr "Bearbeiten"
|
||||||
#: views/lists/forms/forms.hbs:3 views/lists/forms/forms.hbs:5
|
#: views/lists/forms/forms.hbs:3 views/lists/forms/forms.hbs:5
|
||||||
#: views/lists/view.hbs:5
|
#: views/lists/view.hbs:5
|
||||||
msgid "Custom Forms"
|
msgid "Custom Forms"
|
||||||
msgstr "Formulare"
|
msgstr "Benutzerdefinierte Formulare"
|
||||||
|
|
||||||
#: views/lists/forms/create.hbs:4
|
#: views/lists/forms/create.hbs:4
|
||||||
msgid "Create Form"
|
msgid "Create Form"
|
||||||
|
@ -1355,7 +1385,7 @@ msgstr "Formular erstellen"
|
||||||
|
|
||||||
#: views/lists/forms/create.hbs:5 views/lists/forms/forms.hbs:4
|
#: views/lists/forms/create.hbs:5 views/lists/forms/forms.hbs:4
|
||||||
msgid "Create Custom Form"
|
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/create.hbs:6 views/lists/forms/create.hbs:7
|
||||||
#: views/lists/forms/edit.hbs:7 views/lists/forms/edit.hbs:8
|
#: views/lists/forms/edit.hbs:7 views/lists/forms/edit.hbs:8
|
||||||
|
@ -1807,7 +1837,7 @@ msgstr "Deaktiviert"
|
||||||
|
|
||||||
#: views/lists/view.hbs:3
|
#: views/lists/view.hbs:3
|
||||||
msgid "Subscription Form"
|
msgid "Subscription Form"
|
||||||
msgstr "Abonnement-Formular"
|
msgstr "Abonnement Formular"
|
||||||
|
|
||||||
#: views/lists/view.hbs:4
|
#: views/lists/view.hbs:4
|
||||||
msgid "List Actions"
|
msgid "List Actions"
|
||||||
|
@ -1838,7 +1868,7 @@ msgstr "Filter"
|
||||||
|
|
||||||
#: views/lists/view.hbs:17
|
#: views/lists/view.hbs:17
|
||||||
msgid "Subscriptions"
|
msgid "Subscriptions"
|
||||||
msgstr "Abonnemente"
|
msgstr "Abonnements"
|
||||||
|
|
||||||
#: views/lists/view.hbs:18
|
#: views/lists/view.hbs:18
|
||||||
msgid "Imports"
|
msgid "Imports"
|
||||||
|
@ -2107,7 +2137,7 @@ msgstr "Wenn markiert zeigt der Editor HTML-Code ohne Vorschau an"
|
||||||
|
|
||||||
#: views/settings.hbs:11
|
#: views/settings.hbs:11
|
||||||
msgid "Disable subscription confirmation messages"
|
msgid "Disable subscription confirmation messages"
|
||||||
msgstr "Abonnement-Bestätigungsmeldungen deaktivieren"
|
msgstr "Bestätigungsmeldungen für Abonnements deaktivieren"
|
||||||
|
|
||||||
#: views/settings.hbs:12
|
#: views/settings.hbs:12
|
||||||
msgid ""
|
msgid ""
|
||||||
|
@ -2785,6 +2815,10 @@ msgstr "Optionale Kommentare zu dieser Vorlage"
|
||||||
msgid "Back to templates"
|
msgid "Back to templates"
|
||||||
msgstr "Zurück zu Vorlagen"
|
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/create-select.hbs:2 views/triggers/create.hbs:2
|
||||||
#: views/triggers/edit.hbs:2 views/triggers/triggered.hbs:2
|
#: views/triggers/edit.hbs:2 views/triggers/triggered.hbs:2
|
||||||
#: views/triggers/triggers.hbs:2 views/triggers/triggers.hbs:4
|
#: views/triggers/triggers.hbs:2 views/triggers/triggers.hbs:4
|
||||||
|
@ -3072,7 +3106,7 @@ msgstr ""
|
||||||
|
|
||||||
#: views/users/api.hbs:28
|
#: views/users/api.hbs:28
|
||||||
msgid "Remove subscription"
|
msgid "Remove subscription"
|
||||||
msgstr "Abonement entfernen"
|
msgstr "Abonnement entfernen"
|
||||||
|
|
||||||
#: views/users/api.hbs:29
|
#: views/users/api.hbs:29
|
||||||
msgid "This API call marks a subscription as unsubscribed"
|
msgid "This API call marks a subscription as unsubscribed"
|
||||||
|
@ -3493,7 +3527,7 @@ msgstr "Abonnement-ID fehlt"
|
||||||
|
|
||||||
#: lib/models/subscriptions.js:499
|
#: lib/models/subscriptions.js:499
|
||||||
msgid "Missing Subscription email address"
|
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:578 lib/models/subscriptions.js:827
|
||||||
#: lib/models/subscriptions.js:1090
|
#: lib/models/subscriptions.js:1090
|
||||||
|
@ -4108,6 +4142,33 @@ msgstr "Import gestartet"
|
||||||
msgid "Import restarted"
|
msgid "Import restarted"
|
||||||
msgstr "Import neu gestartet"
|
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
|
#: routes/report-templates.js:246
|
||||||
msgid "Could not create report template"
|
msgid "Could not create report template"
|
||||||
msgstr "Report-Vorlage konnte nicht erstellt werden"
|
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);
|
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_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, column, campaign.list, campaign.id, campaign.id, campaign.list, campaign.id, campaign.list, campaign.id, linkId, column, column, column);
|
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();
|
connection.release();
|
||||||
if (err) {
|
if (err) {
|
||||||
return callback(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) => {
|
module.exports.create = (campaign, opts, callback) => {
|
||||||
|
|
||||||
campaign = tools.convertKeys(campaign);
|
campaign = tools.convertKeys(campaign);
|
||||||
|
|
|
@ -9,7 +9,7 @@ let Handlebars = require('handlebars');
|
||||||
let _ = require('../translate')._;
|
let _ = require('../translate')._;
|
||||||
let util = require('util');
|
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;
|
let allowedTypes;
|
||||||
|
|
||||||
module.exports.grouped = ['radio', 'checkbox', 'dropdown'];
|
module.exports.grouped = ['radio', 'checkbox', 'dropdown'];
|
||||||
|
@ -133,10 +133,11 @@ module.exports.create = (listId, field, callback) => {
|
||||||
field.group = null;
|
field.group = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
field.description = (field.description || '').toString().trim() || null;
|
||||||
field.defaultValue = (field.defaultValue || '').toString().trim() || null;
|
field.defaultValue = (field.defaultValue || '').toString().trim() || null;
|
||||||
field.groupTemplate = (field.groupTemplate || '').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) => {
|
module.exports.update = (id, updates, callback) => {
|
||||||
|
@ -157,6 +158,7 @@ module.exports.update = (id, updates, callback) => {
|
||||||
updates.key = slugify(updates.key, '_').toUpperCase();
|
updates.key = slugify(updates.key, '_').toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updates.description = (updates.description || '').toString().trim() || null;
|
||||||
updates.defaultValue = (updates.defaultValue || '').toString().trim() || null;
|
updates.defaultValue = (updates.defaultValue || '').toString().trim() || null;
|
||||||
updates.groupTemplate = (updates.groupTemplate || '').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();
|
type = (type || '').toString().trim().toLowerCase();
|
||||||
group = Number(group) || null;
|
group = Number(group) || null;
|
||||||
listId = Number(listId) || 0;
|
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, '');
|
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(?,?,?,?,?,?,?,?,?)';
|
let query = 'INSERT INTO custom_fields (`list`, `name`, `description`, `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) => {
|
connection.query(query, [listId, name, description, key, defaultValue, type, group, groupTemplate, column, visible ? 1 : 0], (err, result) => {
|
||||||
|
|
||||||
if (err) {
|
if (err) {
|
||||||
connection.release();
|
connection.release();
|
||||||
|
@ -409,6 +411,7 @@ module.exports.getRow = (fieldList, values, useDate, showAll, onlyExisting) => {
|
||||||
id: field.id,
|
id: field.id,
|
||||||
type: field.type,
|
type: field.type,
|
||||||
name: field.name,
|
name: field.name,
|
||||||
|
description: field.description,
|
||||||
column: field.column,
|
column: field.column,
|
||||||
value: (valueList[field.column] || '').toString().trim(),
|
value: (valueList[field.column] || '').toString().trim(),
|
||||||
visible: !!field.visible,
|
visible: !!field.visible,
|
||||||
|
@ -439,6 +442,7 @@ module.exports.getRow = (fieldList, values, useDate, showAll, onlyExisting) => {
|
||||||
id: field.id,
|
id: field.id,
|
||||||
type: field.type,
|
type: field.type,
|
||||||
name: field.name,
|
name: field.name,
|
||||||
|
description: field.description,
|
||||||
column: field.column,
|
column: field.column,
|
||||||
value: (valueList[field.column] || '').toString().trim(),
|
value: (valueList[field.column] || '').toString().trim(),
|
||||||
visible: !!field.visible,
|
visible: !!field.visible,
|
||||||
|
@ -455,6 +459,7 @@ module.exports.getRow = (fieldList, values, useDate, showAll, onlyExisting) => {
|
||||||
id: field.id,
|
id: field.id,
|
||||||
type: field.type,
|
type: field.type,
|
||||||
name: field.name,
|
name: field.name,
|
||||||
|
description: field.description,
|
||||||
column: field.column,
|
column: field.column,
|
||||||
value: Number(valueList[field.column]) || 0,
|
value: Number(valueList[field.column]) || 0,
|
||||||
visible: !!field.visible,
|
visible: !!field.visible,
|
||||||
|
@ -474,6 +479,7 @@ module.exports.getRow = (fieldList, values, useDate, showAll, onlyExisting) => {
|
||||||
id: field.id,
|
id: field.id,
|
||||||
type: field.type,
|
type: field.type,
|
||||||
name: field.name,
|
name: field.name,
|
||||||
|
description: field.description,
|
||||||
visible: !!field.visible,
|
visible: !!field.visible,
|
||||||
key: 'group-g' + field.id,
|
key: 'group-g' + field.id,
|
||||||
mergeTag: field.key,
|
mergeTag: field.key,
|
||||||
|
@ -495,6 +501,7 @@ module.exports.getRow = (fieldList, values, useDate, showAll, onlyExisting) => {
|
||||||
return {
|
return {
|
||||||
type: subField.type,
|
type: subField.type,
|
||||||
name: subField.name,
|
name: subField.name,
|
||||||
|
description: subField.description,
|
||||||
column: subField.column,
|
column: subField.column,
|
||||||
value: valueList[subField.column] ? 1 : 0,
|
value: valueList[subField.column] ? 1 : 0,
|
||||||
visible: !!subField.visible,
|
visible: !!subField.visible,
|
||||||
|
@ -572,6 +579,7 @@ module.exports.getRow = (fieldList, values, useDate, showAll, onlyExisting) => {
|
||||||
id: field.id,
|
id: field.id,
|
||||||
type: field.type,
|
type: field.type,
|
||||||
name: field.name,
|
name: field.name,
|
||||||
|
description: field.description,
|
||||||
column: field.column,
|
column: field.column,
|
||||||
value: useDate ? value : formatted,
|
value: useDate ? value : formatted,
|
||||||
visible: !!field.visible,
|
visible: !!field.visible,
|
||||||
|
|
|
@ -574,11 +574,12 @@ module.exports.getQuery = (id, prefix, callback) => {
|
||||||
|
|
||||||
segment.rules.forEach(rule => {
|
segment.rules.forEach(rule => {
|
||||||
switch (rule.columnType.type) {
|
switch (rule.columnType.type) {
|
||||||
case 'string':
|
case 'string': {
|
||||||
let condition = rule.value.negate ? 'NOT LIKE' : 'LIKE';
|
let condition = rule.value.negate ? 'NOT LIKE' : 'LIKE';
|
||||||
query.push(prefix + '`' + rule.columnType.column + '` ' + condition + ' ?');
|
query.push(prefix + '`' + rule.columnType.column + '` ' + condition + ' ?');
|
||||||
values.push(rule.value.value);
|
values.push(rule.value.value);
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case 'boolean':
|
case 'boolean':
|
||||||
query.push(prefix + '`' + rule.columnType.column + '` = ?');
|
query.push(prefix + '`' + rule.columnType.column + '` = ?');
|
||||||
values.push(rule.value.value);
|
values.push(rule.value.value);
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
{
|
{
|
||||||
"schemaVersion": 33
|
"schemaVersion": 34
|
||||||
}
|
}
|
||||||
|
|
|
@ -106,6 +106,7 @@
|
||||||
"openpgp": "^2.5.11",
|
"openpgp": "^2.5.11",
|
||||||
"passport": "^0.4.0",
|
"passport": "^0.4.0",
|
||||||
"passport-local": "^1.0.0",
|
"passport-local": "^1.0.0",
|
||||||
|
"passport-ldapauth": "^2.0.0",
|
||||||
"premailer-api": "^1.0.4",
|
"premailer-api": "^1.0.4",
|
||||||
"redfour": "^1.0.2",
|
"redfour": "^1.0.2",
|
||||||
"redis": "^2.8.0",
|
"redis": "^2.8.0",
|
||||||
|
|
335
routes/api.js
335
routes/api.js
|
@ -12,29 +12,27 @@ let log = require('npmlog');
|
||||||
let router = new express.Router();
|
let router = new express.Router();
|
||||||
let mailHelpers = require('../lib/subscription-mail-helpers');
|
let mailHelpers = require('../lib/subscription-mail-helpers');
|
||||||
|
|
||||||
|
const handleErrorResponse = (res, log, err, code = 500, message = false) => {
|
||||||
|
if (typeof err != 'undefined')
|
||||||
|
log.error('API', err);
|
||||||
|
res.status(code);
|
||||||
|
return res.json({
|
||||||
|
error: message || err.message || err,
|
||||||
|
data: []
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
router.all('/*', (req, res, next) => {
|
router.all('/*', (req, res, next) => {
|
||||||
if (!req.query.access_token) {
|
if (!req.query.access_token) {
|
||||||
res.status(403);
|
return handleErrorResponse(res, log, false, 403, 'Missing access_token');
|
||||||
return res.json({
|
|
||||||
error: 'Missing access_token',
|
|
||||||
data: []
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
users.findByAccessToken(req.query.access_token, (err, user) => {
|
users.findByAccessToken(req.query.access_token, (err, user) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
res.status(500);
|
return handleErrorResponse(res, log, err);
|
||||||
return res.json({
|
|
||||||
error: err.message || err,
|
|
||||||
data: []
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
if (!user) {
|
if (!user) {
|
||||||
res.status(403);
|
return handleErrorResponse(res, log, false, 403, 'Invalid or expired access_token');
|
||||||
return res.json({
|
|
||||||
error: 'Invalid or expired access_token',
|
|
||||||
data: []
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
@ -48,35 +46,17 @@ router.post('/subscribe/:listId', (req, res) => {
|
||||||
});
|
});
|
||||||
lists.getByCid(req.params.listId, (err, list) => {
|
lists.getByCid(req.params.listId, (err, list) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
log.error('API', err);
|
return handleErrorResponse(res, log, false, 403, 'Invalid or expired access_token');
|
||||||
res.status(500);
|
|
||||||
return res.json({
|
|
||||||
error: err.message || err,
|
|
||||||
data: []
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
if (!list) {
|
if (!list) {
|
||||||
res.status(404);
|
return handleErrorResponse(res, log, false, 404, 'Selected listId not found');
|
||||||
return res.json({
|
|
||||||
error: 'Selected listId not found',
|
|
||||||
data: []
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
if (!input.EMAIL) {
|
if (!input.EMAIL) {
|
||||||
res.status(400);
|
return handleErrorResponse(res, log, false, 400, 'Missing EMAIL');
|
||||||
return res.json({
|
|
||||||
error: 'Missing EMAIL',
|
|
||||||
data: []
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
tools.validateEmail(input.EMAIL, false, err => {
|
tools.validateEmail(input.EMAIL, false, err => {
|
||||||
if (err) {
|
if (err) {
|
||||||
log.error('API', err);
|
return handleErrorResponse(res, log, err, 400);
|
||||||
res.status(400);
|
|
||||||
return res.json({
|
|
||||||
error: err.message || err,
|
|
||||||
data: []
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let subscription = {
|
let subscription = {
|
||||||
|
@ -132,22 +112,12 @@ router.post('/subscribe/:listId', (req, res) => {
|
||||||
|
|
||||||
confirmations.addConfirmation(list.id, 'subscribe', req.ip, data, (err, confirmCid) => {
|
confirmations.addConfirmation(list.id, 'subscribe', req.ip, data, (err, confirmCid) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
log.error('API', err);
|
return handleErrorResponse(res, log, err);
|
||||||
res.status(500);
|
|
||||||
return res.json({
|
|
||||||
error: err.message || err,
|
|
||||||
data: []
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mailHelpers.sendConfirmSubscription(list, input.EMAIL, confirmCid, subscription, (err) => {
|
mailHelpers.sendConfirmSubscription(list, input.EMAIL, confirmCid, subscription, (err) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
log.error('API', err);
|
return handleErrorResponse(res, log, err);
|
||||||
res.status(500);
|
|
||||||
return res.json({
|
|
||||||
error: err.message || err,
|
|
||||||
data: []
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(200);
|
res.status(200);
|
||||||
|
@ -161,12 +131,7 @@ router.post('/subscribe/:listId', (req, res) => {
|
||||||
} else {
|
} else {
|
||||||
subscriptions.insert(list.id, meta, subscription, (err, response) => {
|
subscriptions.insert(list.id, meta, subscription, (err, response) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
log.error('API', err);
|
return handleErrorResponse(res, log, err);
|
||||||
res.status(500);
|
|
||||||
return res.json({
|
|
||||||
error: err.message || err,
|
|
||||||
data: []
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
res.status(200);
|
res.status(200);
|
||||||
res.json({
|
res.json({
|
||||||
|
@ -188,51 +153,26 @@ router.post('/unsubscribe/:listId', (req, res) => {
|
||||||
});
|
});
|
||||||
lists.getByCid(req.params.listId, (err, list) => {
|
lists.getByCid(req.params.listId, (err, list) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
res.status(500);
|
return handleErrorResponse(res, log, err);
|
||||||
return res.json({
|
|
||||||
error: err.message || err,
|
|
||||||
data: []
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
if (!list) {
|
if (!list) {
|
||||||
res.status(404);
|
return handleErrorResponse(res, log, false, 404, 'Selected listId not found');
|
||||||
return res.json({
|
|
||||||
error: 'Selected listId not found',
|
|
||||||
data: []
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
if (!input.EMAIL) {
|
if (!input.EMAIL) {
|
||||||
res.status(400);
|
return handleErrorResponse(res, log, false, 400, 'Missing EMAIL');
|
||||||
return res.json({
|
|
||||||
error: 'Missing EMAIL',
|
|
||||||
data: []
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
subscriptions.getByEmail(list.id, input.EMAIL, (err, subscription) => {
|
subscriptions.getByEmail(list.id, input.EMAIL, (err, subscription) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
res.status(500);
|
return handleErrorResponse(res, log, err);
|
||||||
return res.json({
|
|
||||||
error: err.message || err,
|
|
||||||
data: []
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!subscription) {
|
if (!subscription) {
|
||||||
res.status(404);
|
return handleErrorResponse(res, log, false, 404, 'Subscription with given email not found');
|
||||||
return res.json({
|
|
||||||
error: 'Subscription with given email not found',
|
|
||||||
data: []
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
subscriptions.changeStatus(list.id, subscription.id, false, subscriptions.Status.UNSUBSCRIBED, (err, found) => {
|
subscriptions.changeStatus(list.id, subscription.id, false, subscriptions.Status.UNSUBSCRIBED, (err, found) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
res.status(500);
|
return handleErrorResponse(res, log, err);
|
||||||
return res.json({
|
|
||||||
error: err.message || err,
|
|
||||||
data: []
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
res.status(200);
|
res.status(200);
|
||||||
res.json({
|
res.json({
|
||||||
|
@ -253,55 +193,27 @@ router.post('/delete/:listId', (req, res) => {
|
||||||
});
|
});
|
||||||
lists.getByCid(req.params.listId, (err, list) => {
|
lists.getByCid(req.params.listId, (err, list) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
res.status(500);
|
return handleErrorResponse(res, log, err);
|
||||||
return res.json({
|
|
||||||
error: err.message || err,
|
|
||||||
data: []
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
if (!list) {
|
if (!list) {
|
||||||
res.status(404);
|
return handleErrorResponse(res, log, false, 404, 'Selected listId not found');
|
||||||
return res.json({
|
|
||||||
error: 'Selected listId not found',
|
|
||||||
data: []
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
if (!input.EMAIL) {
|
if (!input.EMAIL) {
|
||||||
res.status(400);
|
return handleErrorResponse(res, log, false, 400, 'Missing EMAIL');
|
||||||
return res.json({
|
|
||||||
error: 'Missing EMAIL',
|
|
||||||
data: []
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
subscriptions.getByEmail(list.id, input.EMAIL, (err, subscription) => {
|
subscriptions.getByEmail(list.id, input.EMAIL, (err, subscription) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
res.status(500);
|
return handleErrorResponse(res, log, err);
|
||||||
return res.json({
|
|
||||||
error: err.message || err,
|
|
||||||
data: []
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
if (!subscription) {
|
if (!subscription) {
|
||||||
res.status(404);
|
return handleErrorResponse(res, log, false, 404, 'Subscription not found');
|
||||||
return res.json({
|
|
||||||
error: 'Subscription not found',
|
|
||||||
data: []
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
subscriptions.delete(list.id, subscription.cid, (err, subscription) => {
|
subscriptions.delete(list.id, subscription.cid, (err, subscription) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
res.status(500);
|
return handleErrorResponse(res, log, err);
|
||||||
return res.json({
|
|
||||||
error: err.message || err,
|
|
||||||
data: []
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
if (!subscription) {
|
if (!subscription) {
|
||||||
res.status(404);
|
return handleErrorResponse(res, log, false, 404, 'Subscription not found');
|
||||||
return res.json({
|
|
||||||
error: 'Subscription not found',
|
|
||||||
data: []
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
res.status(200);
|
res.status(200);
|
||||||
res.json({
|
res.json({
|
||||||
|
@ -320,42 +232,30 @@ router.get('/subscriptions/:listId', (req, res) => {
|
||||||
let limit = parseInt(req.query.limit || 10000, 10);
|
let limit = parseInt(req.query.limit || 10000, 10);
|
||||||
|
|
||||||
lists.getByCid(req.params.listId, (err, list) => {
|
lists.getByCid(req.params.listId, (err, list) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
res.status(500);
|
return handleErrorResponse(res, log, err);
|
||||||
return res.json({
|
}
|
||||||
error: err.message || err,
|
subscriptions.list(list.id, start, limit, (err, rows, total) => {
|
||||||
data: []
|
if (err) {
|
||||||
|
return handleErrorResponse(res, log, err);
|
||||||
|
}
|
||||||
|
res.status(200);
|
||||||
|
res.json({
|
||||||
|
data: {
|
||||||
|
total: total,
|
||||||
|
start: start,
|
||||||
|
limit: limit,
|
||||||
|
subscriptions: rows
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
subscriptions.list(list.id, start, limit, (err, rows, total) => {
|
|
||||||
if (err) {
|
|
||||||
res.status(500);
|
|
||||||
return res.json({
|
|
||||||
error: err.message || err,
|
|
||||||
data: []
|
|
||||||
});
|
|
||||||
}
|
|
||||||
res.status(200);
|
|
||||||
res.json({
|
|
||||||
data: {
|
|
||||||
total: total,
|
|
||||||
start: start,
|
|
||||||
limit: limit,
|
|
||||||
subscriptions: rows
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get('/lists', (req, res) => {
|
router.get('/lists', (req, res) => {
|
||||||
lists.quicklist((err, lists) => {
|
lists.quicklist((err, lists) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
res.status(500);
|
return handleErrorResponse(res, log, err);
|
||||||
return res.json({
|
|
||||||
error: err.message || err,
|
|
||||||
data: []
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
res.status(200);
|
res.status(200);
|
||||||
res.json({
|
res.json({
|
||||||
|
@ -367,10 +267,7 @@ router.get('/lists', (req, res) => {
|
||||||
router.get('/list/:id', (req, res) => {
|
router.get('/list/:id', (req, res) => {
|
||||||
lists.get(req.params.id, (err, list) => {
|
lists.get(req.params.id, (err, list) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
res.status(500);
|
return handleErrorResponse(res, log, err);
|
||||||
return res.json({
|
|
||||||
error: err.message || err,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
res.status(200);
|
res.status(200);
|
||||||
res.json({
|
res.json({
|
||||||
|
@ -382,11 +279,7 @@ router.get('/list/:id', (req, res) => {
|
||||||
router.get('/lists/:email', (req, res) => {
|
router.get('/lists/:email', (req, res) => {
|
||||||
lists.getListsWithEmail(req.params.email, (err, lists) => {
|
lists.getListsWithEmail(req.params.email, (err, lists) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
res.status(500);
|
return handleErrorResponse(res, log, err);
|
||||||
return res.json({
|
|
||||||
error: err.message || err,
|
|
||||||
data: []
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
res.status(200);
|
res.status(200);
|
||||||
res.json({
|
res.json({
|
||||||
|
@ -402,23 +295,15 @@ router.post('/field/:listId', (req, res) => {
|
||||||
});
|
});
|
||||||
lists.getByCid(req.params.listId, (err, list) => {
|
lists.getByCid(req.params.listId, (err, list) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
log.error('API', err);
|
return handleErrorResponse(res, log, err);
|
||||||
res.status(500);
|
|
||||||
return res.json({
|
|
||||||
error: err.message || err,
|
|
||||||
data: []
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
if (!list) {
|
if (!list) {
|
||||||
res.status(404);
|
return handleErrorResponse(res, log, false, 404, 'Selected listId not found');
|
||||||
return res.json({
|
|
||||||
error: 'Selected listId not found',
|
|
||||||
data: []
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let field = {
|
let field = {
|
||||||
name: (input.NAME || '').toString().trim(),
|
name: (input.NAME || '').toString().trim(),
|
||||||
|
description: (input.DESCRIPTION || '').toString().trim(),
|
||||||
defaultValue: (input.DEFAULT || '').toString().trim() || null,
|
defaultValue: (input.DEFAULT || '').toString().trim() || null,
|
||||||
type: (input.TYPE || '').toString().toLowerCase().trim(),
|
type: (input.TYPE || '').toString().toLowerCase().trim(),
|
||||||
group: Number(input.GROUP) || null,
|
group: Number(input.GROUP) || null,
|
||||||
|
@ -428,11 +313,7 @@ router.post('/field/:listId', (req, res) => {
|
||||||
|
|
||||||
fields.create(list.id, field, (err, id, tag) => {
|
fields.create(list.id, field, (err, id, tag) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
res.status(500);
|
return handleErrorResponse(res, log, err);
|
||||||
return res.json({
|
|
||||||
error: err.message || err,
|
|
||||||
data: []
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
res.status(200);
|
res.status(200);
|
||||||
res.json({
|
res.json({
|
||||||
|
@ -451,24 +332,16 @@ router.post('/blacklist/add', (req, res) => {
|
||||||
input[(key || '').toString().trim().toUpperCase()] = (req.body[key] || '').toString().trim();
|
input[(key || '').toString().trim().toUpperCase()] = (req.body[key] || '').toString().trim();
|
||||||
});
|
});
|
||||||
if (!(input.EMAIL) || (input.EMAIL === '')) {
|
if (!(input.EMAIL) || (input.EMAIL === '')) {
|
||||||
res.status(500);
|
return handleErrorResponse(res, log, err);
|
||||||
return res.json({
|
|
||||||
error: 'EMAIL argument are required',
|
|
||||||
data: []
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
blacklist.add(input.EMAIL, (err) =>{
|
blacklist.add(input.EMAIL, (err) =>{
|
||||||
if (err) {
|
if (err) {
|
||||||
res.status(500);
|
return handleErrorResponse(res, log, err);
|
||||||
return res.json({
|
}
|
||||||
error: err.message || err,
|
res.status(200);
|
||||||
data: []
|
res.json({
|
||||||
});
|
data: []
|
||||||
}
|
});
|
||||||
res.status(200);
|
|
||||||
res.json({
|
|
||||||
data: []
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -478,19 +351,11 @@ router.post('/blacklist/delete', (req, res) => {
|
||||||
input[(key || '').toString().trim().toUpperCase()] = (req.body[key] || '').toString().trim();
|
input[(key || '').toString().trim().toUpperCase()] = (req.body[key] || '').toString().trim();
|
||||||
});
|
});
|
||||||
if (!(input.EMAIL) || (input.EMAIL === '')) {
|
if (!(input.EMAIL) || (input.EMAIL === '')) {
|
||||||
res.status(500);
|
return handleErrorResponse(res, log, false, 500, 'EMAIL argument are required');
|
||||||
return res.json({
|
|
||||||
error: 'EMAIL argument are required',
|
|
||||||
data: []
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
blacklist.delete(input.EMAIL, (err) =>{
|
blacklist.delete(input.EMAIL, (err) =>{
|
||||||
if (err) {
|
if (err) {
|
||||||
res.status(500);
|
return handleErrorResponse(res, log, err);
|
||||||
return res.json({
|
|
||||||
error: err.message || err,
|
|
||||||
data: []
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
res.status(200);
|
res.status(200);
|
||||||
res.json({
|
res.json({
|
||||||
|
@ -506,11 +371,7 @@ router.get('/blacklist/get', (req, res) => {
|
||||||
|
|
||||||
blacklist.get(start, limit, search, (err, data, total) => {
|
blacklist.get(start, limit, search, (err, data, total) => {
|
||||||
if (err) {
|
if (err) {
|
||||||
res.status(500);
|
return handleErrorResponse(res, log, err);
|
||||||
return res.json({
|
|
||||||
error: err.message || err,
|
|
||||||
data: []
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
res.status(200);
|
res.status(200);
|
||||||
res.json({
|
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;
|
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) => {
|
router.get('/edit/:id', passport.csrfProtection, (req, res, next) => {
|
||||||
campaigns.get(req.params.id, false, (err, campaign) => {
|
campaigns.get(req.params.id, false, (err, campaign) => {
|
||||||
if (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}}" />
|
<input type="hidden" name="id" value="{{id}}" />
|
||||||
</form>
|
</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}}
|
{{#each attachments}}
|
||||||
<form method="post" id="attachment-download-{{id}}" action="/campaigns/attachment/download">
|
<form method="post" id="attachment-download-{{id}}" action="/campaigns/attachment/download">
|
||||||
<input type="hidden" name="_csrf" value="{{../csrfToken}}">
|
<input type="hidden" name="_csrf" value="{{../csrfToken}}">
|
||||||
|
@ -240,6 +245,7 @@
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<div class="col-sm-offset-2 col-sm-10">
|
<div class="col-sm-offset-2 col-sm-10">
|
||||||
<div class="pull-right">
|
<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>
|
<button type="submit" form="campaigns-delete" class="btn btn-danger"><i class="glyphicon glyphicon-remove"></i> {{#translate}}Delete Campaign{{/translate}}</button>
|
||||||
</div>
|
</div>
|
||||||
<button type="submit" class="btn btn-primary"><i class="glyphicon glyphicon-ok"></i> {{#translate}}Update{{/translate}}</button>
|
<button type="submit" class="btn btn-primary"><i class="glyphicon glyphicon-ok"></i> {{#translate}}Update{{/translate}}</button>
|
||||||
|
|
|
@ -19,6 +19,13 @@
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="form-group">
|
||||||
<label for="type" class="col-sm-2 control-label">{{#translate}}Field Type{{/translate}}</label>
|
<label for="type" class="col-sm-2 control-label">{{#translate}}Field Type{{/translate}}</label>
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-10">
|
||||||
|
|
|
@ -26,6 +26,13 @@
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="form-group">
|
||||||
<label for="type" class="col-sm-2 control-label">{{#translate}}Field Type{{/translate}}</label>
|
<label for="type" class="col-sm-2 control-label">{{#translate}}Field Type{{/translate}}</label>
|
||||||
<div class="col-sm-10">
|
<div class="col-sm-10">
|
||||||
|
|
|
@ -36,7 +36,7 @@
|
||||||
{{#translate}}Equals{{/translate}}
|
{{#translate}}Equals{{/translate}}
|
||||||
</option>
|
</option>
|
||||||
<option value="1" {{#if value.negate}} selected {{/if}}>
|
<option value="1" {{#if value.negate}} selected {{/if}}>
|
||||||
{{#translate}}Not squals{{/translate}}
|
{{#translate}}Not equals{{/translate}}
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -111,7 +111,7 @@
|
||||||
$.post('/editorapi/update?type={{type}}&editor={{resource.editorName}}', {
|
$.post('/editorapi/update?type={{type}}&editor={{resource.editorName}}', {
|
||||||
id: {{resource.id}},
|
id: {{resource.id}},
|
||||||
name: '{{resource.name}}',
|
name: '{{resource.name}}',
|
||||||
{{#if resource.list}} list: {{resource.list}}, {{/if}}
|
{{#if resource.list}} list: {{resource.list}}+':'+{{resource.segment}}, {{/if}}
|
||||||
html: html,
|
html: html,
|
||||||
editorData: JSON.stringify({
|
editorData: JSON.stringify({
|
||||||
template: '{{resource.editorData.template}}',
|
template: '{{resource.editorData.template}}',
|
||||||
|
|
|
@ -134,9 +134,12 @@
|
||||||
<label>{{name}}</label>
|
<label>{{name}}</label>
|
||||||
<input type="hidden" name="origin_{{key}}" value="webform">
|
<input type="hidden" name="origin_{{key}}" value="webform">
|
||||||
{{#each options}}
|
{{#each options}}
|
||||||
|
<p>
|
||||||
<label class="label-checkbox">
|
<label class="label-checkbox">
|
||||||
<input type="checkbox" name="{{column}}" value="1" {{#if value}} checked {{/if}}> {{name}}
|
<input type="checkbox" name="{{column}}" value="1" {{#if value}} checked {{/if}}> {{name}}
|
||||||
</label>
|
</label>
|
||||||
|
<span class="help-block">{{description}}</span>
|
||||||
|
</p>
|
||||||
{{/each}}
|
{{/each}}
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
|
@ -181,6 +181,7 @@
|
||||||
</p>
|
</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li><strong>NAME</strong> – {{#translate}}field name{{/translate}} (<em>{{#translate}}required{{/translate}}</em>)</li>
|
<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}}
|
<li><strong>TYPE</strong> – {{#translate}}one of the following types:{{/translate}}
|
||||||
<ul>
|
<ul>
|
||||||
{{#each allowedTypes}}
|
{{#each allowedTypes}}
|
||||||
|
@ -333,4 +334,33 @@
|
||||||
<strong>{{#translate}}Example{{/translate}}</strong>
|
<strong>{{#translate}}Example{{/translate}}</strong>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<pre>curl -XGET '{{serviceUrl}}api/list/1?access_token={{accessToken}}'</pre>
|
<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