Compare commits

...
Sign in to create a new pull request.

53 commits

Author SHA1 Message Date
d563928b0b Update 'Dockerfile' 2020-09-17 08:47:16 +00:00
Andris Reinman
281072ac1b
Merge pull request #910 from Mailtrain-org/GHSL-2020-132
Remove extra variables from format call
2020-07-13 12:51:21 +03:00
Andris Reinman
be55f2ca52 remove extra variables from format call 2020-07-13 12:50:34 +03:00
Andris Reinman
dea056848a
Merge pull request #909 from Mailtrain-org/GHSL-2020-132
Use proper escaping for variable column names
2020-07-13 10:07:16 +03:00
Andris Reinman
b7f94b40d6 Use proper escaping for variable column names 2020-07-13 10:00:25 +03:00
Tomas Bures
3b365701bb
Merge pull request #715 from fwolfst/patch-1
Update README
2019-10-02 23:58:23 +03:00
Tomas Bures
62e63fbb80
Merge pull request #633 from ateuber/bugfix-mosaico-segments
Bugfix: include list segment when saving in Mosaico
2019-07-16 18:31:07 +02:00
Andreas Teuber
0a0eb0db4f Bugfix: include list segment when saving in Mosaico 2019-07-16 18:21:17 +02:00
Tomas Bures
4e298aca7b
Merge pull request #625 from ezzra/german_language
modify german language files
2019-07-03 20:32:40 +02:00
ezzra
50e52207f9 add more translations to german, modify some others 2019-06-28 18:56:41 +02:00
ezzra
cf6c0e6849 modify german language files 2019-06-28 11:44:58 +02:00
Tomas Bures
9ca4767b4e
Merge pull request #614 from dkorts1/patch-3
Update README.md
2019-06-15 14:35:41 +02:00
Tomas Bures
d685c8e61f
Update README.md 2019-06-15 14:34:59 +02:00
dkorts1
cce887361b
Update README.md
minor changes, grammar, and typos
2019-06-14 01:04:34 +03:00
Tomas Bures
6d75ce035d
Merge pull request #610 from softionsrl/patch-1
Standardization of error handling
2019-06-12 17:44:06 +02:00
Softion
0db686d63c
Standardization of error handling
Probably not the best way, but this should make the code more readable as a lot of it is just outputting error messages.
This allows for centralized error messages translation.
2019-06-12 17:38:45 +02:00
Tomas Bures
a376dce0c1
Merge pull request #597 from kuba-orlik/master
Add a Polish translation for 'It looks like you are already subscribed…'
2019-05-12 10:02:34 +02:00
Kuba Orlik
c9383cb6da Update the translation in the missing .mo file, as well
Without that the new translation does not show up
2019-05-09 14:06:00 +02:00
Kuba Orlik
4d25685d69 Add a Polish translation for 'It looks like you are already subscribed to this list.'
Because it looked out of place when it was displayed on the subscription widget
2019-05-09 13:25:55 +02:00
Tomas Bures
db9c3271f5
Merge pull request #578 from paszczus/master
removed moved Polish translation files
2019-04-16 14:58:33 +02:00
Sławomir Paszkiewicz
5eb7976a75 removed moved Polish translation files 2019-04-02 20:25:29 +02:00
Tomas Bures
47802a7933
Merge pull request #575 from paszczus/master
moved PL language files to proper place
2019-04-02 16:24:16 +02:00
Sławomir Paszkiewicz
cda2beb5f5 - moved PL language files to proper place 2019-04-02 11:15:58 +02:00
Felix Wolfsteller
e2a9cc9429
Update README
The sentences are a bit long, but it prevents default installations to have issues with image uploads (#374).
2019-03-19 09:47:58 +01:00
Tomas Bures
c3cf46a717
Merge pull request #477 from ganeshkrishnan1/patch-1
path of conf files
2019-03-09 09:07:50 +01:00
Tomas Bures
4353201ab8
Merge pull request #496 from arnobase/french_typo_correction
update french translation and a typo in rule-edit.hbs
2019-03-09 09:07:21 +01:00
Tomas Bures
d45b1fa762
Merge pull request #479 from devMCpro/polish-translation
Polish translation
2019-03-09 09:06:43 +01:00
Tomas Bures
e74401b25d
Merge pull request #480 from ateuber/master
API: Add the possibility to change the email address of an existing list subscriber
2019-03-09 09:06:19 +01:00
Tomas Bures
33f94034e2
Merge branch 'master' into master 2019-03-09 09:06:06 +01:00
Tomas Bures
fa1bf1c874
Merge pull request #482 from ateuber/custom_field_description
Custom field description
2019-03-09 09:04:38 +01:00
Tomas Bures
de78c587f5
Merge pull request #556 from ateuber/duplicate_campaign
Duplicate campaign
2019-03-09 09:04:08 +01:00
Andreas Teuber
4a446a99f0 Fix Travis 2019-03-07 19:45:59 +01:00
Andreas Teuber
2b11a319b4 Added possibility to duplicate a campaign 2019-03-07 19:40:31 +01:00
Tomas Bures
f2ed0e8ce2
Update README.md 2018-12-25 21:08:24 +01:00
Tomas Bures
ebd7b1fffb
Update README.md 2018-12-25 20:49:18 +01:00
Arno
a49a9b2637 update french translation and a typo in views/lists/segments/rule-edit.hbs 2018-11-29 17:28:33 +01:00
Andreas Teuber
5b24186240 Adapted API description 2018-10-31 16:37:52 +01:00
Andreas Teuber
d3ad3e5d68 Fix Travis 2018-10-31 14:06:30 +01:00
Andreas Teuber
cdaf4b0b16 Added description to custom fields 2018-10-31 11:18:31 +01:00
Andreas Teuber
d5ce6a5d33 Fix Travis 2018-10-23 17:01:08 +02:00
Andreas Teuber
dd696d49ac Added API call to change the email address of an existing list subscriber 2018-10-23 15:51:54 +02:00
Marcin
5394ada6ad
Polish translation 2018-10-22 14:14:31 +02:00
Ganesh Krishnan
ce10388f4f
path of conf files
these conf files have moved from inside of docs to setup folder.
2018-10-18 18:49:01 -04:00
Tomas Bures
f661ba8a6b
Merge pull request #466 from balping/patch-1
Ignore more config files
2018-09-27 18:34:30 +02:00
Tomas Bures
26b3cd44e8
Merge pull request #465 from balping/trans
Hungarian translation
2018-09-27 18:34:04 +02:00
Balázs Kovács
6bdf074c01 translate more strings 2018-09-10 16:10:28 +02:00
Balázs Dura-Kovács
f89ba5a5dd
ignore more config files 2018-09-08 15:52:46 +02:00
Balázs Kovács
5db5bb73c4 hungarian trans continued 2018-09-07 16:54:29 +02:00
Tomas Bures
dcf8330929
Merge pull request #458 from a4i/master
add french translation
2018-09-03 07:26:03 +02:00
Tomas Bures
a774975f45
Merge pull request #457 from Addy90/patch-1
add passport-ldapauth to dependencies
2018-09-03 07:25:23 +02:00
Sylvain Amrani
af8c3202dd add french translation 2018-08-11 19:32:49 +02:00
Adrian
4dd754ff82
add passport-ldapauth to dependencies
when "npm install --production" is called, every time, passport-ldapauth is removed from local repository, with adding the dependency to package.json, the behavior is as expected (see production.yml config for ldap config)
2018-08-07 08:41:13 +02:00
Balázs Kovács
920f833988 Hungarian trans wip 2018-07-29 03:07:10 +02:00
27 changed files with 13490 additions and 315 deletions

6
.gitignore vendored
View file

@ -5,9 +5,9 @@ node_modules
npm-debug.log
package-lock.json
.DS_Store
config/development.*
config/production.*
config/test.*
config/development*
config/production*
config/test*
workers/reports/config/development.*
workers/reports/config/production.*
workers/reports/config/test.*

View file

@ -1,13 +1,47 @@
FROM node:8.6
# Mutistaged Node.js Build
FROM node:10-alpine as builder
# Install system dependencies
RUN set -ex; \
apk add --update --no-cache \
make gcc g++ git python
# Copy package.json dependencies
COPY server/package.json /app/server/package.json
COPY server/package-lock.json /app/server/package-lock.json
COPY client/package.json /app/client/package.json
COPY client/package-lock.json /app/client/package-lock.json
COPY shared/package.json /app/shared/package.json
COPY shared/package-lock.json /app/shared/package-lock.json
COPY zone-mta/package.json /app/zone-mta/package.json
COPY zone-mta/package-lock.json /app/zone-mta/package-lock.json
# Install dependencies in each directory
RUN cd /app/client && npm install
RUN cd /app/shared && npm install --production
RUN cd /app/server && npm install --production
RUN cd /app/zone-mta && npm install --production
# First install dependencies
COPY ./package.json ./app/
WORKDIR /app/
ENV NODE_ENV production
RUN npm install --no-progress --production && npm install --no-progress passport-ldapjs passport-ldapauth
# Later, copy the app files. That improves development speed as buiding the Docker image will not have
# to download and install all the NPM dependencies every time there's a change in the source code
COPY . /app
EXPOSE 3000
RUN set -ex; \
cd /app/client && \
npm run build && \
rm -rf node_modules
# Final Image
FROM node:10-alpine
WORKDIR /app/
# Install system dependencies
RUN set -ex; \
apk add --update --no-cache \
pwgen netcat-openbsd bash imagemagick
COPY --from=builder /app/ /app/
EXPOSE 3000 3003 3004
ENTRYPOINT ["bash", "/app/docker-entrypoint.sh"]
CMD ["node", "index.js"]

View file

@ -1,3 +1,5 @@
## Mailtrain v2 beta is available. Check it out here https://github.com/Mailtrain-org/mailtrain/tree/development
# Mailtrain
[Mailtrain](http://mailtrain.org) is a self hosted newsletter application built on Node.js (v7+) and MySQL (v5.5+ or MariaDB).
@ -33,7 +35,7 @@ Depending on how you have configured your system and Docker you may need to prep
* Start: `docker-compose start`
* Open [http://localhost:3000/](http://localhost:3000/) (change the host name `localhost` to the name of the host where you are deploying the system).
* Authenticate as user `admin` with password `test`
* Navigate to [http://localhost:3000/settings](http://localhost:3000/settings) and update service configuration.
* Navigate to [http://localhost:3000/settings](http://localhost:3000/settings) and update service configuration, especially replace the value in `Service Address (URL)` from `localhost` to the actual IP or domain if installed on a server, otherwise e.g. image upload will not work.
* Navigate to [http://localhost:3000/users/account](http://localhost:3000/users/account) and update user information and password.
## Quick Start - Manual Install (any OS that supports Node.js)
@ -47,7 +49,7 @@ Depending on how you have configured your system and Docker you may need to prep
4. Run the server `NODE_ENV=production npm start`
5. Open [http://localhost:3000/](http://localhost:3000/)
6. Authenticate as `admin`:`test`
7. Navigate to [http://localhost:3000/settings](http://localhost:3000/settings) and update service configuration
7. Navigate to [http://localhost:3000/settings](http://localhost:3000/settings) and update service configuration, especially replace the value in `Service Address (URL)` from `localhost` to the actual IP or domain if installed on a server, otherwise e.g. image upload will not work.
8. Navigate to [http://localhost:3000/users/account](http://localhost:3000/users/account) and update user information and password
## Read The Docs

View file

@ -1,4 +1,4 @@
# Mailtrain
# Mailtrain (v1)
[Mailtrain](http://mailtrain.org) is a self hosted newsletter application built on Node.js (v7+) and MySQL (v5.5+ or MariaDB).
@ -42,7 +42,7 @@ Check out [ZoneMTA](https://github.com/zone-eu/zone-mta) as an alternative self
## Cons
* Beta-grade software. Might or might not work as expected. There are several users with list sizes between 100k and 1M and Mailtrain seems to work for them but YMMV
* Beta-grade software. Several users reported success with lists of various sizes (from 100k to 1M) however there is no absolute guarantee it will always work as expected.
* Almost no documentation (there are some guides in the [Wiki](https://github.com/Mailtrain-org/mailtrain/wiki))
## Requirements
@ -76,13 +76,13 @@ Install script installs and sets up the following:
* **logrotate** to rotate Mailtrain log files
* **upstart** or **systemd** init script to automatically start and manage Mailtrain process
After the install script has finished and you see a success message then you should have a Mailtrain instance running at http://yourdomain.com
After the install script has finished and you have received "successfully installed" message, you should have a Mailtrain instance running at http://yourdomain.com
#### Next steps after installation
##### 1. Change admin password
Navigate to http://yourdomain.com where yourdomain.com is the address of your server. Click on the Sign In link in the right top corner of the page. Authenticate with the following credentials:
Navigate to http://yourdomain.com where yourdomain.com is the address of your server. Click on the **Sign In** link in the right top corner of the page. Authenticate with the following credentials:
* Username: **admin**
* Password: **test**
@ -91,7 +91,7 @@ Once authenticated, click on your username in the right top corner of the page a
##### 2. Update page configuration
If signed in navigate to http://yourdomain.com/settings and check that all email addresses and domain names are correct. Mailtrain default installation comes bundled with [ZoneMTA](https://github.com/zone-eu/zone-mta), so you should be able to send out messages right away. ZoneMTA even handles a lot of bounces (not all kind of bounces though) automatically so you do not have to change anything in the SMTP settings to get going.
If signed in, navigate to http://yourdomain.com/settings and check that all email addresses and domain names are correct. Mailtrain default installation comes bundled with [ZoneMTA](https://github.com/zone-eu/zone-mta), so you should be able to send out messages right away. ZoneMTA even handles a lot of bounces (not all kind of bounces though) automatically so you do not have to change anything in the SMTP settings to get going.
##### 3. Set up SPF
@ -103,9 +103,9 @@ If you are using the bundled ZoneMTA then you can provide a DKIM key to sign all
##### 5. Set up VERP
The bundled ZoneMTA can already handle a large amount of bounces if you use it to deliver messages but not all - namely such bounces that happen *after* the recipient MX accepts the message for local delivery. This might happen for example when a user exists, so the MX accepts the message but the quota for that user is checked only when actually storing the message to users' mailbox. Then a bounce message is generated and sent to the original sender which in your case is the mail address you are sending your list messages from. You can catch these messages and mark such recipients manually as bounced but alternatively you can set up a VERP based bounce handler that does this automatically. In this case the sender on the message envelope would not be your actual address but a rewritten bounce address that points to your Mailtrain installation.
The bundled ZoneMTA can already handle a large amount of bounces if you use it to deliver messages but not all: namely, such bounces that happen *after* the recipient MX accepts the message for local delivery. This might happen for example when a user exists, so the MX accepts the message but the quota for that user is checked only when actually storing the message to users' mailbox. Then a bounce message is generated and sent to the original sender which in your case is the mail address you are sending your list messages from. You can catch these messages and mark such recipients manually as bounced but alternatively you can set up a VERP based bounce handler that does this automatically. In this case the sender on the message envelope would not be your actual address but a rewritten bounce address that points to your Mailtrain installation.
To set it up you need to create an additonal DNS MX entry for a bounce domain, eg "bounces.example.com" if you are sending from "example.com". This entry should point to your Mailtrain server IP address. Next you should enable the VERP handling in Mailtrain Settings page.
To set it up you need to create an additonal DNS MX entry for a bounce domain, eg "bounces.example.com",if you are sending from "example.com". This entry should point to your Mailtrain server IP address. Next you should enable the VERP handling in Mailtrain Settings page.
> As ZoneMTA uses envelope sender as the default for DKIM addresses, then if using VERP you need to set up DKIM to your bounce domain instead of sender domain and also store the DKIM key as "bouncedomain.selector.pem" in the ZoneMTA key folder.
@ -123,7 +123,7 @@ With proper SPF, DKIM and PTR records (DMARC wouldn't hurt either) I got perfect
DKIM, DMARK, SPF and PTR are DNS records which spam filters use to figure out if e-mails were really sent by you (and not by a spammer who tries to conceal his identity to be able to continue send bulks of e-mails people never subscribed for). Assuming that you use zone-mta and your e-mails are to originate from a Mailtrain installation at `mailtrain.example.com` and optionally from `mail.example.net`, to practically set all these records up you will need to:
1. generate genrate a private and public DKIM key
**1.generate a private and public DKIM key**
```sh
mkdir /opt/dkim-keys
@ -133,7 +133,7 @@ openssl genrsa -out mailtrain.example.com.key 2048 # private key mailtrain.examp
openssl rsa -in mailtrain.example.com.key -out mailtrain.example.com.pub -pubout -outform PEM # public key mailtrain.example.com.pub
```
2. add 3 new txt records for the mailtrain.example.com that will most likely similar to the example below:
**2.add the three new txt records for the mailtrain.example.com that will most likely look similar to the example below**
```
default._domainkey.mailtrain.example.com TXT "k=rsa; p=[public key in one line];"
@ -151,10 +151,10 @@ The above steps will have the following effect:
- all messages sent by Mailtrain / Zone-mta will be signed by the DKIM Private Key (the signature becomes a part of the e-mail)
- when a spamfilter encounters this signature, it will look for the **<DKIM selector>**._domainkey.**<DKIM domain>** TXT record, and use the public key stored there to verify that the signature is valid
- additionally, the spamfilter will look for a TXT SPF record and will look a if the e-mail was sent from the IP address of mailtrain.example.com or mail.example.net. If the sender IP or domain is different, it will discard the e-mail as spam.
- additionally, the spamfilter will look for a TXT SPF record and will look if the e-mail was sent from the IP address of mailtrain.example.com or mail.example.net. If the sender IP or domain is different, it will discard the e-mail as spam.
- furthermore, the spamfilter looks for the DMARC record, which tells it what to do with mails that aren't signed with DKIM or which don't have a valid signature. The example above will tell the spamfilter to reject such a mail as well.
3. You are now almost set. To further confirm that you have full control over your network, the last step is to set up a PTR record, which will give the right answer for a reverse DNS lookup (answer to "what domain name is bound to IP address xxx.xxx.xxx.xxx). If you run your own DNS, you probably know it will look similar to this:
You are now almost set. To further confirm that you have full control over your network, the last step is to set up a PTR record, which will give the right answer for a reverse DNS lookup (answer to "what domain name is bound to IP address xxx.xxx.xxx.xxx). If you run your own DNS, you probably know it will look similar to this:
```
10.27/1.110.220.in-addr.arpa. 1800 PTR mailtrain.example.com.
@ -172,7 +172,7 @@ If you run Mailtrain on a VPS, you will have to find the PTR configuration somew
#### Install:
* Download Mailtrain files using git: `git clone git://github.com/Mailtrain-org/mailtrain.git` (or download [zipped repo](https://github.com/Mailtrain-org/mailtrain/archive/master.zip)) and open Mailtrain folder `cd mailtrain`
* **Note**: depending on how you have configured your system and Docker you may need to prepend the commands below with `sudo`.
* **Note**: depending on how you have configured your system and Docker, you may need to prepend the commands below with `sudo`.
* Copy the file `docker-compose.override.yml.tmpl` to `docker-compose.override.yml` and modify it if you need to.
* Bring up the stack with: `docker-compose up -d`, by default it will use the included `docker-compose.yml` file and override some configurations taken from the `docker-compose.override.yml` file.
* If you want to use only / copy the `docker-compose.yml` file (for example, if you were deploying with Rancher), you may need to first run `docker-compose build` to make sure your system has a Docker image `mailtrain:latest`.
@ -197,7 +197,7 @@ If you run Mailtrain on a VPS, you will have to find the PTR configuration somew
## 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
## Using Environment Variables
@ -221,11 +221,11 @@ Mailtrain uses [node-config](https://github.com/lorenwest/node-config) for confi
### Running Behind Nginx Proxy
Edit [mailtrain.nginx](setup/mailtrain-nginx.conf) (update `server_name` directive) and copy it to `/etc/nginx/sites-enabled`
Edit [mailtrain.nginx](../setup/mailtrain-nginx.conf) (update `server_name` directive) and copy it to `/etc/nginx/sites-enabled`
### Running as an Upstart Service in Ubuntu 14.04
Edit [mailtrain.conf](setup/mailtrain.conf) (update application folder) and copy it to `/etc/init`
Edit [mailtrain.conf](../setup/mailtrain.conf) (update application folder) and copy it to `/etc/init`
## Subscription Widget
@ -273,7 +273,7 @@ If using VERP with iRedMail, see [this post](http://www.iredmail.org/forum/post4
## Testing
There is a built in /dev/null server in Mailtrain that you can use to load test your installation. Check the `[testserver]` section in the configuration file for details. By default the test server is disabled. The server uses only cleartext connections, so select "Do not use encryption" in the encryption settings when setting up the server data in Mailtrain.
There is a built in /dev/null server in Mailtrain that you can use to load-test your installation. Check the `[testserver]` section in the configuration file for details. By default, the test server is disabled. The server uses only cleartext connections, so select: "Do not use encryption" in the encryption settings when setting up the server data in Mailtrain.
Additionally you can generate CSV import files with fake subscriber data:
@ -285,7 +285,7 @@ This command generates a CSV file with 100 000 subscriber accounts
## Translations
Mailtrain is currently not translated but it supports translations. To add translations you first need to add translation support for the translatable strings. To test if strings are translatable or not, use a fake language with code "zz"
Mailtrain is currently not translated but it supports translations. To add translations you first need to add translation support for the translatable strings. To test if strings are translatable or not, use a fake language with the code "zz".
```toml
language="zz"
@ -327,10 +327,11 @@ Enclose translatable strings to `{{#translate}}` tags
* To create the translation catalog run `grunt` from command line. This fetches all translatable strings from JavaScript and Handlebars files and merges these into the translation catalog located at */languages/mailtrain.pot*
* To add a new language use this catalog file as source. Once you want to update your translation file from the updated catalog, then select "Catalogue" -> "Update from POT file..." in POEdit and select mailtrain.pot. This would merge all new translations from the POT file to your PO file.
* To add a new language use this catalog file as a source. Once you want to update your translation file from the updated catalog, select "Catalogue" -> "Update from POT file..." in POEdit and select mailtrain.pot. This will merge all the new translations from the POT file to your PO file.
*If you have saved the PO file in [./languages](./languages) then POEdit should auto generate required MO file whenever you hit save for the PO file.
* Once you have a correct MO file in the languages folder, then edit Mailtrain config and set ["language" option](https://github.com/Mailtrain-org/mailtrain/blob/ba8bd1212335cb9bd7ba094beb7b5400f35cae6c/config/default.toml#L30-L31) to your language name. If the value is "et" then Mailtrain loads translations from ./languages/et.mo
* Once you have a correct MO file in the languages folder, edit Mailtrain config and set ["language" option](https://github.com/Mailtrain-org/mailtrain/blob/ba8bd1212335cb9bd7ba094beb7b5400f35cae6c/config/default.toml#L30-L31) to your language name. If the value is "et" then Mailtrain loads translations from ./languages/et.mo
> **NB!** For now translation settings are global, so if you have set a translation in config then this applies to all users. An user can't select another translation than the default even if there is a translation file. This is because current Mailtrain code does not provide request context to functions and the functions generating strings do not know which language to use.

Binary file not shown.

View file

@ -58,7 +58,7 @@ msgstr "Blacklist"
#: views/blacklist.hbs:4 views/users/api.hbs:55
msgid "Add email to blacklist"
msgstr "E-Mail zur Blacklist hinzufügen"
msgstr "E-Mail Adresse zur Blacklist hinzufügen"
#: views/blacklist.hbs:5
msgid "Add"
@ -151,7 +151,7 @@ msgstr "SMTP Antwort"
#: views/campaigns/bounced.hbs:11
msgid "Bounce time"
msgstr "Bouncen Zeitpunkt"
msgstr "Bounce Zeitpunkt"
#: views/campaigns/campaigns.hbs:3 views/campaigns/create-triggered.hbs:25
#: views/campaigns/create.hbs:3 views/campaigns/create.hbs:4
@ -289,8 +289,8 @@ msgid ""
"new entry is found from this feed it is sent to selected list as an email "
"message."
msgstr ""
"RSS-Kampagne setzt einen Tracker gegen den gewählten RSS-Feed. Wenn ein "
"neuer Eintrag aus diesem Feed gefunden wird, wird er als E-Mail an die "
"Eine RSS-Kampagne setzt einen Tracker auf den gewählten RSS-Feed. Wenn ein "
"neuer Eintrag in diesem Feed gefunden wird, wird er als E-Mail an die "
"ausgewählte Liste gesendet."
#: views/campaigns/create-rss.hbs:7 views/campaigns/create-triggered.hbs:6
@ -363,21 +363,21 @@ msgstr ""
#: views/campaigns/edit-triggered.hbs:16 views/campaigns/edit.hbs:17
#: views/campaigns/view.hbs:12
msgid "Email \"from name\""
msgstr "E-Mail \"von\" Name"
msgstr "E-Mail \"von Name\""
#: views/campaigns/create-rss.hbs:16 views/campaigns/create-triggered.hbs:19
#: views/campaigns/create.hbs:19 views/campaigns/edit-rss.hbs:19
#: views/campaigns/edit-triggered.hbs:17 views/campaigns/edit.hbs:18
#: views/settings.hbs:23
msgid "This is the name your emails will come from"
msgstr "Dies ist Absendernamen Ihrer E-Mails"
msgstr "Dies ist der Absendername Ihrer E-Mails"
#: views/campaigns/create-rss.hbs:17 views/campaigns/create-triggered.hbs:20
#: views/campaigns/create.hbs:20 views/campaigns/edit-rss.hbs:20
#: views/campaigns/edit-triggered.hbs:18 views/campaigns/edit.hbs:19
#: views/campaigns/view.hbs:13
msgid "Email \"from\" address"
msgstr "E-Mail \"von\" Adresse"
msgstr "E-Mail \"von Adresse\""
#: views/campaigns/create-rss.hbs:18 views/campaigns/create-triggered.hbs:21
#: views/campaigns/edit-rss.hbs:21 views/campaigns/edit-triggered.hbs:19
@ -386,10 +386,16 @@ msgid "This is the address people will send replies to"
msgstr "Dies ist die Reply-To Adresse"
#: views/campaigns/create-rss.hbs:19 views/campaigns/create-triggered.hbs:24
#: views/campaigns/create.hbs:26 views/campaigns/edit-rss.hbs:22
#: views/campaigns/edit-triggered.hbs:22 views/campaigns/edit.hbs:25
msgid "Disable clicked/opened tracking"
msgstr "Tracking deaktivieren"
#: views/campaigns/create.hbs:28 views/campaigns/edit-rss.hbs:22
#: views/campaigns/edit-triggered.hbs:22 views/campaigns/edit.hbs:27
msgid "Disable opened tracking"
msgstr "Deaktiviere das Tracking beim Öffnen der E-Mail (Zählpixel)"
#: views/campaigns/create-rss.hbs:20 views/campaigns/create-triggered.hbs:25
#: views/campaigns/create.hbs:29 views/campaigns/edit-rss.hbs:23
#: views/campaigns/edit-triggered.hbs:23 views/campaigns/edit.hbs:28
msgid "Disable clicked tracking"
msgstr "Deaktiviere das Tracking beim Klicken von Links"
#: views/campaigns/create-triggered.hbs:3
#: views/campaigns/create-triggered.hbs:4
@ -410,12 +416,12 @@ msgstr "Vorlage auswählen:"
#: views/campaigns/create-triggered.hbs:15 views/campaigns/create.hbs:15
msgid "Selecting a template creates a campaign specific copy from it"
msgstr ""
"Wenn Sie eine Vorlage auswählen, wird eine kampagenspezifische Kopie erstellt"
"Wenn Sie eine Vorlage auswählen, wird eine Kopie nur für diese Kampagne erstellt"
#: views/campaigns/create-triggered.hbs:16 views/campaigns/create.hbs:16
msgid "Or alternatively use an URL as the message content source:"
msgstr ""
"Oder alternativ können Sie eine URL als E-Mail-Inhalts-Quelle verwenden:"
"Oder alternativ können Sie eine URL als Quelle für den E-Mail Inhalt verwenden:"
#: views/campaigns/create-triggered.hbs:17 views/campaigns/create.hbs:17
#: views/campaigns/edit-triggered.hbs:25 views/campaigns/edit.hbs:28
@ -431,13 +437,13 @@ msgstr ""
#: views/campaigns/edit-triggered.hbs:20 views/campaigns/edit.hbs:23
#: views/campaigns/view.hbs:15
msgid "Email \"subject line\""
msgstr "E-Mail-Betreffzeile"
msgstr "E-Mail Betreffzeile"
#: views/campaigns/create-triggered.hbs:23 views/campaigns/create.hbs:25
#: views/campaigns/edit-triggered.hbs:21 views/campaigns/edit.hbs:24
#: views/settings.hbs:27
msgid "Keep it relevant and non-spammy"
msgstr "Halten Sie den Inhalt relevant und non-spammy"
msgstr "Halten Sie den Inhalt relevant und frei von Spam"
#: views/campaigns/create.hbs:21 views/campaigns/edit.hbs:20
msgid ""
@ -445,7 +451,7 @@ msgid ""
"set"
msgstr ""
"Dies ist die Adresse, an welche Antworten gesendet werden, ausser die "
"Kampagnenspezifische reply-to Adresse ist definiert"
"Kampagnenspezifische \"reply-to\" Adresse ist definiert"
#: views/campaigns/create.hbs:22 views/campaigns/edit.hbs:21
#: views/campaigns/view.hbs:14
@ -458,15 +464,23 @@ msgstr ""
"Falls gesetzt, ist dies die Adresse an welche E-Mail-Antworten gesendet "
"werden"
#: views/campaigns/create.hbs:26 views/campaigns/edit.hbs:25
msgid "Custom unsubscribe (URL)"
msgstr "Benutzerdefiniertes Deabonnement (URL)"
#: views/campaigns/create.hbs:27 views/campaigns/edit.hbs:26
msgid "Set a custom unsubscribe url"
msgstr "Geben Sie eine eigene URL an, die den Abonnenten zum Abmelden angezeigt wird"
#: views/campaigns/delivered.hbs:3 views/campaigns/delivered.hbs:4
msgid "Delivered info"
msgstr "Zustellungs-Info"
msgstr "Zustellungs Info"
#: views/campaigns/delivered.hbs:6
msgid "Subscribers who received the message and did not bounce/unsubscribe:"
msgstr ""
"Abonnenten, welche die Nachricht erhalten haben und nicht bounced oder "
"abbestellen:"
"Abonnenten, welche die Nachricht erfolgreich erhalten (kein \"bounce\") und "
"nicht deabonniert haben:"
#: views/campaigns/delivered.hbs:11
msgid "Delivery time"
@ -516,7 +530,7 @@ msgstr "Generell"
#: views/campaigns/edit-triggered.hbs:23 views/campaigns/edit.hbs:26
msgid "Template Settings"
msgstr "Vorlagen-Einstellungen"
msgstr "Vorlagen Einstellungen"
#: views/campaigns/edit-triggered.hbs:24 views/campaigns/edit.hbs:27
msgid "Template URL"
@ -568,7 +582,7 @@ msgstr "Anzahl Öffnungen"
#: views/campaigns/unsubscribed.hbs:3 views/campaigns/unsubscribed.hbs:4
msgid "Unsubscribed info"
msgstr "Abmeldungs-Info"
msgstr "Deabonnement Info"
#: views/campaigns/unsubscribed.hbs:6
msgid "Subscribers who unsubscribed:"
@ -577,7 +591,7 @@ msgstr "Abonnenten welche deabonnierten:"
#: views/campaigns/unsubscribed.hbs:11 views/campaigns/view.hbs:28
#: views/lists/subscription/import.hbs:10 routes/lists.js:202
msgid "Unsubscribed"
msgstr "Abbestellt"
msgstr "Deabonniert"
#: views/campaigns/upload-attachment.hbs:7
msgid "Upload"
@ -619,7 +633,7 @@ msgstr "Kampagnen Vorschau als"
#: views/campaigns/view.hbs:17
msgid "Add new test user"
msgstr "Neuen Test-User hinzufügen"
msgstr "Neuen Testbenutzer hinzufügen"
#: views/campaigns/view.hbs:18
msgid "No test users yet, create one here"
@ -643,7 +657,7 @@ msgstr "Blacklisted"
#: views/campaigns/view.hbs:23
msgid "List subscribers who blacklisted by global blacklist"
msgstr ""
msgstr "Abonnenten, die von der globalen Blacklist aufgelistet wurden"
#: views/campaigns/view.hbs:24 routes/lists.js:202
msgid "Bounced"
@ -685,7 +699,7 @@ msgstr "Abonnenten, die auf einen Link geklickt haben"
msgid ""
"Are you sure? This action would start sending messages to the selected list"
msgstr ""
"Sind Sie sicher? Diese Aktion würde mit dem Senden von Nachrichten an die "
"Sind Sie sicher? Diese Aktion wird mit dem Senden von Nachrichten an die "
"ausgewählte Liste beginnen"
#: views/campaigns/view.hbs:35
@ -706,7 +720,7 @@ msgstr "An Abonnenten senden:"
#: views/campaigns/view.hbs:39
msgid "Are you sure? This action would reset scheduling"
msgstr "Sind Sie sicher? Diese Aktion würde die Terminierung zurücksetzen"
msgstr "Sind Sie sicher? Diese Aktion wird die Terminierung zurücksetzen"
#: views/campaigns/view.hbs:40
msgid "Cancel"
@ -728,7 +742,7 @@ msgstr "Am senden"
msgid ""
"Are you sure? This action would resume sending messages to the selected list"
msgstr ""
"Sind Sie sicher? Diese Aktion würde das Senden von E-Mails an die "
"Sind Sie sicher? Diese Aktion wird das Senden von E-Mails an die "
"ausgewählte Liste fortsetzen"
#: views/campaigns/view.hbs:45 views/campaigns/view.hbs:49
@ -762,7 +776,7 @@ msgid ""
"Are you sure? This action would pause sending new entries in RSS feed as "
"email messages to the selected list"
msgstr ""
"Sind Sie sicher? Diese Aktion würde das Senden neuer Einträge des RSS-Feed "
"Sind Sie sicher? Diese Aktion wird das Senden neuer Einträge des RSS-Feed "
"als E-Mail-Nachrichten an die ausgewählte Liste pausieren"
#: views/campaigns/view.hbs:55 views/campaigns/view.hbs:59
@ -830,7 +844,7 @@ msgstr "Ändern Sie Ihr Passwort"
#: views/emails/password-reset-text.hbs:2
msgid "We have received a password change request for your Mailtrain account:"
msgstr ""
"Wir haben eine Passwortänderungsanforderung für Ihr Mailtrain-Konto erhalten:"
"Wir haben eine Anforderung zur Passwortänderungs für Ihr Mailtrain-Konto erhalten:"
#: views/emails/password-reset-html.hbs:3
#: views/emails/password-reset-text.hbs:3
@ -843,8 +857,8 @@ msgid ""
"If you did not ask to change your password, then you can ignore this email "
"and your password will not be changed."
msgstr ""
"Wenn Sie nicht angefordert haben, Ihr Passwort zu ändern, können Sie diese E-"
"Mail ignorieren und Ihr Passwort wird nicht geändert."
"Wenn Sie Ihr Passwort nicht ändern wollen, können Sie diese E-"
"Mail einfach ignorieren."
#: views/emails/rss-html.hbs:1 views/emails/stationery-html.hbs:3
#: views/emails/stationery-text.hbs:3
@ -864,7 +878,7 @@ msgstr "Newsletter abbestellen"
#: views/emails/rss-html.hbs:3 views/emails/stationery-html.hbs:5
#: views/emails/stationery-text.hbs:5
msgid "View this email in your browser"
msgstr "E-Mail in Browser ansehen"
msgstr "E-Mail im Browser ansehen"
#: views/emails/stationery-html.hbs:1 views/emails/stationery-text.hbs:1
msgid "Hey [FIRST_NAME/Customer],"
@ -885,10 +899,10 @@ msgid ""
"API or import from a CSV file. All lists come with support for custom fields "
"and merge tags as well."
msgstr ""
"Mailtrain ermöglicht es Ihnen, auch sehr große Listen zu verwalten. Million "
"Mailtrain ermöglicht es Ihnen, auch sehr große Listen zu verwalten. Millionen von "
"Abonnenten? Kein Problem. Sie können Abonnenten manuell über die API "
"hinzufügen oder aus einer CSV-Datei importieren. Alle Listen kommen mit "
"Unterstützung für benutzerdefinierte Felder und Merge-Tags."
"hinzufügen oder aus einer CSV-Datei importieren. Für alle Listen können "
"zusätzliche Datenfelder und Merge Tags genutzt werden."
#: views/index.hbs:3 views/index.hbs:7 views/index.hbs:10 views/index.hbs:13
#: views/index.hbs:16 views/index.hbs:19 views/index.hbs:22 views/index.hbs:25
@ -900,7 +914,7 @@ msgstr "Zeige mehr"
#: views/lists/fields/edit.hbs:3 views/lists/fields/fields.hbs:3
#: views/lists/fields/fields.hbs:5 views/lists/view.hbs:6
msgid "Custom Fields"
msgstr "Felder"
msgstr "Zusätzliche Datenfelder"
#: views/index.hbs:5
msgid ""
@ -908,8 +922,8 @@ msgid ""
"Every custom field can be included in the generated newsletters through "
"merge tags."
msgstr ""
"Textfelder, Nummern, Dropdowns oder Checkboxen, Mailtrain hat sie alle. "
"Jedes benutzerdefinierte Feld kann in den generierten Newslettern mittels "
"Textfelder, Nummern, Dropdowns oder Checkboxen, Mailtrain bietet all das. "
"Jedes zusätzliche Feld kann in den generierten Newslettern über "
"Merge-Tags eingebunden werden."
#: views/index.hbs:6
@ -918,7 +932,7 @@ msgstr "Mailtrain bietet auch benutzerdefinierte Formulare."
#: views/index.hbs:8
msgid "List Segmentation"
msgstr "Segmentierung"
msgstr "Listen Segmentierung"
#: views/index.hbs:9
msgid ""
@ -939,13 +953,13 @@ msgid ""
"then Mailtrain auto-generates a new campaign using entry data as message "
"contents and sends it to selected subscribers."
msgstr ""
"Mailtrain kann RSS-Feeds verfolgen, und wenn ein neuer Eintrag in einem Feed "
"erkannt wird, sendet Mailtrain automatisch eine neue Kampagne mit dem RSS-"
"Beitrag als Nachrichteninhalt an die ausgewählte Liste."
"Mailtrain kann RSS-Feeds verfolgen und bei neuen Einträgen automatisch "
"eine neue Kampagne mit dem RSS-Beitrag als Nachrichteninhalt an die "
"ausgewählte Liste senden."
#: views/index.hbs:14
msgid "GPG Encryption"
msgstr "GPG-Verschlüsselung"
msgstr "GPG Verschlüsselung"
#: views/index.hbs:15
msgid ""
@ -954,7 +968,7 @@ msgid ""
msgstr ""
"Wenn für eine Liste ein benutzerdefiniertes Feld für den GPG-Public-Key "
"vorhanden ist, können Abonnenten ihren GPG-Public-Key hochladen, um "
"verschlüsselte E-Mails der Liste zu empfangen."
"alle E-Mails verschlüsselt zu erhalten."
#: views/index.hbs:17
msgid "Click Stats"
@ -984,7 +998,7 @@ msgstr ""
#: views/index.hbs:23
msgid "Send via Any Provider"
msgstr "Sende mit"
msgstr "Sende mit beliebigen Anbietern"
#: views/index.hbs:24
msgid ""
@ -1015,7 +1029,7 @@ msgstr ""
#: views/index.hbs:29
msgid "Donate to Author"
msgstr "Dem Autor Spenden"
msgstr "Dem Autor spenden"
#: views/index.hbs:30
msgid "Mailtrain is available under GPLv3 license and completely open source."
@ -1127,6 +1141,15 @@ msgstr "Name der Liste"
msgid "Allow public users to subscribe themselves"
msgstr "Allen erlauben, diese Liste selbst zu abonnieren"
#: views/lists/create.hbs:11 views/lists/edit.hbs:17
msgid "Unsubscription"
msgstr "Deabonnement"
#: views/lists/create.hbs:12 views/lists/edit.hbs:18
msgid "Select how an unsuscription request by subscriber is handled."
msgstr ""
"Wählen Sie, welche Schritte zum Abmelden von der Liste notwendig sein sollen."
#: views/lists/edit.hbs:3 views/lists/edit.hbs:4 views/lists/view.hbs:8
msgid "Edit List"
msgstr "Liste bearbeiten"
@ -1145,7 +1168,7 @@ msgstr "Diese Listen ID wird den Abonnenten dargestellt"
#: views/lists/edit.hbs:12
msgid "Custom Form"
msgstr "Formular"
msgstr "Benutzerdefiniertes Formular"
#: views/lists/edit.hbs:13 views/lists/forms/forms.hbs:11
msgid "Default Mailtrain Form"
@ -1153,11 +1176,18 @@ msgstr "Standard Mailtrain Formular"
#: views/lists/edit.hbs:14
msgid ""
"The custom form used for this list. You can create a form <a href=\"/forms/"
"{{id}}/create\">here</a>."
"The custom form used for this list. You can create a form <a href=\"/forms/{{id}}/create\">here</a>."
msgstr ""
"Das Standard-Formular dieser Liste. Wenn Sie ein Formular erstellt möchten, "
"klicken Sie <a href=\"/forms/{{id}}/create\">hier</a>."
"Das Standard-Formular dieser Liste. Wenn Sie ein benutzerdefiniertes Formular "
"erstellen möchten, klicken Sie <a href=\"/forms/{{id}}/create\">hier</a>."
#: views/lists/edit.hbs:19
msgid "Unsubscribe Header"
msgstr "Deabonnement Header"
#: views/lists/edit.hbs:20
msgid "Do not send List-Unsubscribe headers"
msgstr "Sende keine Header zum Abmelden von der Liste"
#: views/lists/edit.hbs:16
msgid "Delete List"
@ -1169,7 +1199,7 @@ msgstr "Feld erstellen"
#: views/lists/fields/create.hbs:5 views/lists/fields/fields.hbs:4
msgid "Create Custom Field"
msgstr "Feld erstellen"
msgstr "zusäzliches Feld erstellen"
#: views/lists/fields/create.hbs:6 views/lists/fields/create.hbs:7
#: views/lists/fields/edit.hbs:7 views/lists/fields/edit.hbs:8
@ -1247,7 +1277,7 @@ msgstr "Dropdown"
#: views/lists/fields/create.hbs:23 views/lists/fields/edit.hbs:24
#: lib/models/fields.js:22
msgid "Radio Buttons"
msgstr "Radio Knöpfe"
msgstr "Radio Buttons"
#: views/lists/fields/create.hbs:24 views/lists/fields/edit.hbs:25
#: lib/models/fields.js:23
@ -1270,7 +1300,7 @@ msgstr "Erforderlich für Gruppenoptionen"
#: views/lists/fields/edit.hbs:35 views/lists/fields/edit.hbs:36
#: views/lists/fields/fields.hbs:9
msgid "Default merge tag value"
msgstr "Standard-Merge-Tag-Wert"
msgstr "Standard Wert für Merge-Tag"
#: views/lists/fields/create.hbs:32 views/lists/fields/edit.hbs:34
msgid ""
@ -1305,7 +1335,7 @@ msgstr "Feld bearbeiten"
#: views/lists/fields/edit.hbs:5
msgid "Edit Custom Field"
msgstr "Feld bearbeiten"
msgstr "Zusätzliches Feld bearbeiten"
#: views/lists/fields/edit.hbs:6
msgid "Back to fields"
@ -1347,7 +1377,7 @@ msgstr "Bearbeiten"
#: views/lists/forms/forms.hbs:3 views/lists/forms/forms.hbs:5
#: views/lists/view.hbs:5
msgid "Custom Forms"
msgstr "Formulare"
msgstr "Benutzerdefinierte Formulare"
#: views/lists/forms/create.hbs:4
msgid "Create Form"
@ -1355,7 +1385,7 @@ msgstr "Formular erstellen"
#: views/lists/forms/create.hbs:5 views/lists/forms/forms.hbs:4
msgid "Create Custom Form"
msgstr "Formular erstellen"
msgstr "Benutzerdefiniertes Formular erstellen"
#: views/lists/forms/create.hbs:6 views/lists/forms/create.hbs:7
#: views/lists/forms/edit.hbs:7 views/lists/forms/edit.hbs:8
@ -1807,7 +1837,7 @@ msgstr "Deaktiviert"
#: views/lists/view.hbs:3
msgid "Subscription Form"
msgstr "Abonnement-Formular"
msgstr "Abonnement Formular"
#: views/lists/view.hbs:4
msgid "List Actions"
@ -1838,7 +1868,7 @@ msgstr "Filter"
#: views/lists/view.hbs:17
msgid "Subscriptions"
msgstr "Abonnemente"
msgstr "Abonnements"
#: views/lists/view.hbs:18
msgid "Imports"
@ -2107,7 +2137,7 @@ msgstr "Wenn markiert zeigt der Editor HTML-Code ohne Vorschau an"
#: views/settings.hbs:11
msgid "Disable subscription confirmation messages"
msgstr "Abonnement-Bestätigungsmeldungen deaktivieren"
msgstr "Bestätigungsmeldungen für Abonnements deaktivieren"
#: views/settings.hbs:12
msgid ""
@ -2785,6 +2815,10 @@ msgstr "Optionale Kommentare zu dieser Vorlage"
msgid "Back to templates"
msgstr "Zurück zu Vorlagen"
#: views/templates/edit.hbs:11
msgid "Duplicate"
msgstr "Duplizieren"
#: views/triggers/create-select.hbs:2 views/triggers/create.hbs:2
#: views/triggers/edit.hbs:2 views/triggers/triggered.hbs:2
#: views/triggers/triggers.hbs:2 views/triggers/triggers.hbs:4
@ -3072,7 +3106,7 @@ msgstr ""
#: views/users/api.hbs:28
msgid "Remove subscription"
msgstr "Abonement entfernen"
msgstr "Abonnement entfernen"
#: views/users/api.hbs:29
msgid "This API call marks a subscription as unsubscribed"
@ -3493,7 +3527,7 @@ msgstr "Abonnement-ID fehlt"
#: lib/models/subscriptions.js:499
msgid "Missing Subscription email address"
msgstr "Abonnement-E-Mail-Adresse fehlt"
msgstr "Abonnement E-Mail Adresse fehlt"
#: lib/models/subscriptions.js:578 lib/models/subscriptions.js:827
#: lib/models/subscriptions.js:1090
@ -4108,6 +4142,33 @@ msgstr "Import gestartet"
msgid "Import restarted"
msgstr "Import neu gestartet"
#: routes/lists.js:797
msgid "One-step (i.e. no email with confirmation link)"
msgstr "Ein Schritt ohne Formular (direkt nach dem Klicken auf den Link erfolgt die Abmeldung)"
#: routes/lists.js:803
msgid ""
"One-step with unsubscription form (i.e. no email with confirmation link)"
msgstr ""
"Ein Schritt mit Formular (nach dem Klick wird ein Formular mit der E-Mail Adresse angezeigt)"
#: routes/lists.js:809
msgid "Two-step (i.e. an email with confirmation link will be sent)"
msgstr "Zwei Schritte ohne Formular (direkt nach dem Klick auf den Link wird eine E-Mail mit Bestätigungslink versendet)"
#: routes/lists.js:815
msgid ""
"Two-step with unsubscription form (i.e. an email with confirmation link will "
"be sent)"
msgstr ""
"Zwei Schritte mit Formular (nach dem Klick wird ein Formular mit der E-Mail Adresse " "angezeigt, dann wird eine E-Mail mit Bestätigungslink versendet)"
#: routes/lists.js:821
msgid ""
"Manual (i.e. unsubscription has to be performed by the list administrator)"
msgstr ""
"Manuell (die Abmeldung muss durch den Listen Admin vorgenommen werden)"
#: routes/report-templates.js:246
msgid "Could not create report template"
msgstr "Report-Vorlage konnte nicht erstellt werden"

BIN
languages/fr_FR.mo Normal file

Binary file not shown.

4796
languages/fr_FR.po Normal file

File diff suppressed because it is too large Load diff

BIN
languages/hu_HU.mo Normal file

Binary file not shown.

4521
languages/hu_HU.po Normal file

File diff suppressed because it is too large Load diff

BIN
languages/pl_PL.mo Normal file

Binary file not shown.

3739
languages/pl_PL.po Normal file

File diff suppressed because it is too large Load diff

View file

@ -60,10 +60,10 @@ module.exports.statsClickedSubscribersByColumn = (campaign, linkId, request, col
return callback(err);
}
let query_template = 'SELECT %s AS data, COUNT(*) AS cnt FROM `subscription__%d` JOIN `campaign_tracker__%d` ON `campaign_tracker__%d`.`list`=%d AND `campaign_tracker__%d`.`subscriber`=`subscription__%d`.`id` AND `campaign_tracker__%d`.`link`=%d GROUP BY `%s` ORDER BY COUNT(`%s`) DESC, `%s`';
let query = util.format(query_template, column, campaign.list, campaign.id, campaign.id, campaign.list, campaign.id, campaign.list, campaign.id, linkId, column, column, column);
let query_template = 'SELECT ?? AS data, COUNT(*) AS cnt FROM `subscription__%d` JOIN `campaign_tracker__%d` ON `campaign_tracker__%d`.`list`=%d AND `campaign_tracker__%d`.`subscriber`=`subscription__%d`.`id` AND `campaign_tracker__%d`.`link`=%d GROUP BY ?? ORDER BY COUNT(??) DESC, ??';
let query = util.format(query_template, campaign.list, campaign.id, campaign.id, campaign.list, campaign.id, campaign.list, campaign.id, linkId);
connection.query(query, (err, rows) => {
connection.query(query, [column, column, column, column], (err, rows) => {
connection.release();
if (err) {
return callback(err);
@ -365,6 +365,17 @@ module.exports.getLinks = (id, linkId, callback) => {
});
};
module.exports.duplicate = (id, callback) => module.exports.get(id, true, (err, campaign) => {
if (err) {
return callback(err);
}
if (!campaign) {
return callback(new Error(_('Campaign does not exist')));
}
campaign.name = campaign.name + ' Copy';
return module.exports.create(campaign, false, callback);
});
module.exports.create = (campaign, opts, callback) => {
campaign = tools.convertKeys(campaign);

View file

@ -9,7 +9,7 @@ let Handlebars = require('handlebars');
let _ = require('../translate')._;
let util = require('util');
let allowedKeys = ['name', 'key', 'default_value', 'group', 'group_template', 'visible'];
let allowedKeys = ['name', 'description', 'key', 'default_value', 'group', 'group_template', 'visible'];
let allowedTypes;
module.exports.grouped = ['radio', 'checkbox', 'dropdown'];
@ -133,10 +133,11 @@ module.exports.create = (listId, field, callback) => {
field.group = null;
}
field.description = (field.description || '').toString().trim() || null;
field.defaultValue = (field.defaultValue || '').toString().trim() || null;
field.groupTemplate = (field.groupTemplate || '').toString().trim() || null;
addCustomField(listId, field.name, field.defaultValue, field.type, field.group, field.groupTemplate, field.visible, callback);
addCustomField(listId, field.name, field.description, field.defaultValue, field.type, field.group, field.groupTemplate, field.visible, callback);
};
module.exports.update = (id, updates, callback) => {
@ -157,6 +158,7 @@ module.exports.update = (id, updates, callback) => {
updates.key = slugify(updates.key, '_').toUpperCase();
}
updates.description = (updates.description || '').toString().trim() || null;
updates.defaultValue = (updates.defaultValue || '').toString().trim() || null;
updates.groupTemplate = (updates.groupTemplate || '').toString().trim() || null;
@ -277,7 +279,7 @@ module.exports.delete = (fieldId, callback) => {
});
};
function addCustomField(listId, name, defaultValue, type, group, groupTemplate, visible, callback) {
function addCustomField(listId, name, description, defaultValue, type, group, groupTemplate, visible, callback) {
type = (type || '').toString().trim().toLowerCase();
group = Number(group) || null;
listId = Number(listId) || 0;
@ -314,8 +316,8 @@ function addCustomField(listId, name, defaultValue, type, group, groupTemplate,
column = ('custom_' + slugify(name, '_') + '_' + shortid.generate()).toLowerCase().replace(/[^a-z0-9_]/g, '');
}
let query = 'INSERT INTO custom_fields (`list`, `name`, `key`,`default_value`, `type`, `group`, `group_template`, `column`, `visible`) VALUES(?,?,?,?,?,?,?,?,?)';
connection.query(query, [listId, name, key, defaultValue, type, group, groupTemplate, column, visible ? 1 : 0], (err, result) => {
let query = 'INSERT INTO custom_fields (`list`, `name`, `description`, `key`,`default_value`, `type`, `group`, `group_template`, `column`, `visible`) VALUES(?,?,?,?,?,?,?,?,?,?)';
connection.query(query, [listId, name, description, key, defaultValue, type, group, groupTemplate, column, visible ? 1 : 0], (err, result) => {
if (err) {
connection.release();
@ -409,6 +411,7 @@ module.exports.getRow = (fieldList, values, useDate, showAll, onlyExisting) => {
id: field.id,
type: field.type,
name: field.name,
description: field.description,
column: field.column,
value: (valueList[field.column] || '').toString().trim(),
visible: !!field.visible,
@ -439,6 +442,7 @@ module.exports.getRow = (fieldList, values, useDate, showAll, onlyExisting) => {
id: field.id,
type: field.type,
name: field.name,
description: field.description,
column: field.column,
value: (valueList[field.column] || '').toString().trim(),
visible: !!field.visible,
@ -455,6 +459,7 @@ module.exports.getRow = (fieldList, values, useDate, showAll, onlyExisting) => {
id: field.id,
type: field.type,
name: field.name,
description: field.description,
column: field.column,
value: Number(valueList[field.column]) || 0,
visible: !!field.visible,
@ -474,6 +479,7 @@ module.exports.getRow = (fieldList, values, useDate, showAll, onlyExisting) => {
id: field.id,
type: field.type,
name: field.name,
description: field.description,
visible: !!field.visible,
key: 'group-g' + field.id,
mergeTag: field.key,
@ -495,6 +501,7 @@ module.exports.getRow = (fieldList, values, useDate, showAll, onlyExisting) => {
return {
type: subField.type,
name: subField.name,
description: subField.description,
column: subField.column,
value: valueList[subField.column] ? 1 : 0,
visible: !!subField.visible,
@ -572,6 +579,7 @@ module.exports.getRow = (fieldList, values, useDate, showAll, onlyExisting) => {
id: field.id,
type: field.type,
name: field.name,
description: field.description,
column: field.column,
value: useDate ? value : formatted,
visible: !!field.visible,

View file

@ -574,11 +574,12 @@ module.exports.getQuery = (id, prefix, callback) => {
segment.rules.forEach(rule => {
switch (rule.columnType.type) {
case 'string':
case 'string': {
let condition = rule.value.negate ? 'NOT LIKE' : 'LIKE';
query.push(prefix + '`' + rule.columnType.column + '` ' + condition + ' ?');
values.push(rule.value.value);
break;
}
case 'boolean':
query.push(prefix + '`' + rule.columnType.column + '` = ?');
values.push(rule.value.value);

View file

@ -1,3 +1,3 @@
{
"schemaVersion": 33
"schemaVersion": 34
}

View file

@ -106,6 +106,7 @@
"openpgp": "^2.5.11",
"passport": "^0.4.0",
"passport-local": "^1.0.0",
"passport-ldapauth": "^2.0.0",
"premailer-api": "^1.0.4",
"redfour": "^1.0.2",
"redis": "^2.8.0",

View file

@ -12,29 +12,27 @@ let log = require('npmlog');
let router = new express.Router();
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) => {
if (!req.query.access_token) {
res.status(403);
return res.json({
error: 'Missing access_token',
data: []
});
return handleErrorResponse(res, log, false, 403, 'Missing access_token');
}
users.findByAccessToken(req.query.access_token, (err, user) => {
if (err) {
res.status(500);
return res.json({
error: err.message || err,
data: []
});
return handleErrorResponse(res, log, err);
}
if (!user) {
res.status(403);
return res.json({
error: 'Invalid or expired access_token',
data: []
});
return handleErrorResponse(res, log, false, 403, 'Invalid or expired access_token');
}
next();
});
@ -48,35 +46,17 @@ router.post('/subscribe/:listId', (req, res) => {
});
lists.getByCid(req.params.listId, (err, list) => {
if (err) {
log.error('API', err);
res.status(500);
return res.json({
error: err.message || err,
data: []
});
return handleErrorResponse(res, log, false, 403, 'Invalid or expired access_token');
}
if (!list) {
res.status(404);
return res.json({
error: 'Selected listId not found',
data: []
});
return handleErrorResponse(res, log, false, 404, 'Selected listId not found');
}
if (!input.EMAIL) {
res.status(400);
return res.json({
error: 'Missing EMAIL',
data: []
});
return handleErrorResponse(res, log, false, 400, 'Missing EMAIL');
}
tools.validateEmail(input.EMAIL, false, err => {
if (err) {
log.error('API', err);
res.status(400);
return res.json({
error: err.message || err,
data: []
});
return handleErrorResponse(res, log, err, 400);
}
let subscription = {
@ -132,22 +112,12 @@ router.post('/subscribe/:listId', (req, res) => {
confirmations.addConfirmation(list.id, 'subscribe', req.ip, data, (err, confirmCid) => {
if (err) {
log.error('API', err);
res.status(500);
return res.json({
error: err.message || err,
data: []
});
return handleErrorResponse(res, log, err);
}
mailHelpers.sendConfirmSubscription(list, input.EMAIL, confirmCid, subscription, (err) => {
if (err) {
log.error('API', err);
res.status(500);
return res.json({
error: err.message || err,
data: []
});
return handleErrorResponse(res, log, err);
}
res.status(200);
@ -161,12 +131,7 @@ router.post('/subscribe/:listId', (req, res) => {
} else {
subscriptions.insert(list.id, meta, subscription, (err, response) => {
if (err) {
log.error('API', err);
res.status(500);
return res.json({
error: err.message || err,
data: []
});
return handleErrorResponse(res, log, err);
}
res.status(200);
res.json({
@ -188,51 +153,26 @@ router.post('/unsubscribe/:listId', (req, res) => {
});
lists.getByCid(req.params.listId, (err, list) => {
if (err) {
res.status(500);
return res.json({
error: err.message || err,
data: []
});
return handleErrorResponse(res, log, err);
}
if (!list) {
res.status(404);
return res.json({
error: 'Selected listId not found',
data: []
});
return handleErrorResponse(res, log, false, 404, 'Selected listId not found');
}
if (!input.EMAIL) {
res.status(400);
return res.json({
error: 'Missing EMAIL',
data: []
});
return handleErrorResponse(res, log, false, 400, 'Missing EMAIL');
}
subscriptions.getByEmail(list.id, input.EMAIL, (err, subscription) => {
if (err) {
res.status(500);
return res.json({
error: err.message || err,
data: []
});
return handleErrorResponse(res, log, err);
}
if (!subscription) {
res.status(404);
return res.json({
error: 'Subscription with given email not found',
data: []
});
return handleErrorResponse(res, log, false, 404, 'Subscription with given email not found');
}
subscriptions.changeStatus(list.id, subscription.id, false, subscriptions.Status.UNSUBSCRIBED, (err, found) => {
if (err) {
res.status(500);
return res.json({
error: err.message || err,
data: []
});
return handleErrorResponse(res, log, err);
}
res.status(200);
res.json({
@ -253,55 +193,27 @@ router.post('/delete/:listId', (req, res) => {
});
lists.getByCid(req.params.listId, (err, list) => {
if (err) {
res.status(500);
return res.json({
error: err.message || err,
data: []
});
return handleErrorResponse(res, log, err);
}
if (!list) {
res.status(404);
return res.json({
error: 'Selected listId not found',
data: []
});
return handleErrorResponse(res, log, false, 404, 'Selected listId not found');
}
if (!input.EMAIL) {
res.status(400);
return res.json({
error: 'Missing EMAIL',
data: []
});
return handleErrorResponse(res, log, false, 400, 'Missing EMAIL');
}
subscriptions.getByEmail(list.id, input.EMAIL, (err, subscription) => {
if (err) {
res.status(500);
return res.json({
error: err.message || err,
data: []
});
return handleErrorResponse(res, log, err);
}
if (!subscription) {
res.status(404);
return res.json({
error: 'Subscription not found',
data: []
});
return handleErrorResponse(res, log, false, 404, 'Subscription not found');
}
subscriptions.delete(list.id, subscription.cid, (err, subscription) => {
if (err) {
res.status(500);
return res.json({
error: err.message || err,
data: []
});
return handleErrorResponse(res, log, err);
}
if (!subscription) {
res.status(404);
return res.json({
error: 'Subscription not found',
data: []
});
return handleErrorResponse(res, log, false, 404, 'Subscription not found');
}
res.status(200);
res.json({
@ -320,42 +232,30 @@ router.get('/subscriptions/:listId', (req, res) => {
let limit = parseInt(req.query.limit || 10000, 10);
lists.getByCid(req.params.listId, (err, list) => {
if (err) {
res.status(500);
return res.json({
error: err.message || err,
data: []
if (err) {
return handleErrorResponse(res, log, err);
}
subscriptions.list(list.id, start, limit, (err, rows, total) => {
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) => {
lists.quicklist((err, lists) => {
if (err) {
res.status(500);
return res.json({
error: err.message || err,
data: []
});
return handleErrorResponse(res, log, err);
}
res.status(200);
res.json({
@ -367,10 +267,7 @@ router.get('/lists', (req, res) => {
router.get('/list/:id', (req, res) => {
lists.get(req.params.id, (err, list) => {
if (err) {
res.status(500);
return res.json({
error: err.message || err,
});
return handleErrorResponse(res, log, err);
}
res.status(200);
res.json({
@ -382,11 +279,7 @@ router.get('/list/:id', (req, res) => {
router.get('/lists/:email', (req, res) => {
lists.getListsWithEmail(req.params.email, (err, lists) => {
if (err) {
res.status(500);
return res.json({
error: err.message || err,
data: []
});
return handleErrorResponse(res, log, err);
}
res.status(200);
res.json({
@ -402,23 +295,15 @@ router.post('/field/:listId', (req, res) => {
});
lists.getByCid(req.params.listId, (err, list) => {
if (err) {
log.error('API', err);
res.status(500);
return res.json({
error: err.message || err,
data: []
});
return handleErrorResponse(res, log, err);
}
if (!list) {
res.status(404);
return res.json({
error: 'Selected listId not found',
data: []
});
return handleErrorResponse(res, log, false, 404, 'Selected listId not found');
}
let field = {
name: (input.NAME || '').toString().trim(),
description: (input.DESCRIPTION || '').toString().trim(),
defaultValue: (input.DEFAULT || '').toString().trim() || null,
type: (input.TYPE || '').toString().toLowerCase().trim(),
group: Number(input.GROUP) || null,
@ -428,11 +313,7 @@ router.post('/field/:listId', (req, res) => {
fields.create(list.id, field, (err, id, tag) => {
if (err) {
res.status(500);
return res.json({
error: err.message || err,
data: []
});
return handleErrorResponse(res, log, err);
}
res.status(200);
res.json({
@ -451,24 +332,16 @@ router.post('/blacklist/add', (req, res) => {
input[(key || '').toString().trim().toUpperCase()] = (req.body[key] || '').toString().trim();
});
if (!(input.EMAIL) || (input.EMAIL === '')) {
res.status(500);
return res.json({
error: 'EMAIL argument are required',
data: []
});
return handleErrorResponse(res, log, err);
}
blacklist.add(input.EMAIL, (err) =>{
if (err) {
res.status(500);
return res.json({
error: err.message || err,
data: []
});
}
res.status(200);
res.json({
data: []
});
if (err) {
return handleErrorResponse(res, log, err);
}
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();
});
if (!(input.EMAIL) || (input.EMAIL === '')) {
res.status(500);
return res.json({
error: 'EMAIL argument are required',
data: []
});
return handleErrorResponse(res, log, false, 500, 'EMAIL argument are required');
}
blacklist.delete(input.EMAIL, (err) =>{
if (err) {
res.status(500);
return res.json({
error: err.message || err,
data: []
});
return handleErrorResponse(res, log, err);
}
res.status(200);
res.json({
@ -506,11 +371,7 @@ router.get('/blacklist/get', (req, res) => {
blacklist.get(start, limit, search, (err, data, total) => {
if (err) {
res.status(500);
return res.json({
error: err.message || err,
data: []
});
return handleErrorResponse(res, log, err);
}
res.status(200);
res.json({
@ -524,4 +385,66 @@ router.get('/blacklist/get', (req, res) => {
});
});
router.post('/changeemail/:listId', (req, res) => {
let input = {};
Object.keys(req.body).forEach(key => {
input[(key || '').toString().trim().toUpperCase()] = (req.body[key] || '').toString().trim();
});
if (!(input.EMAILOLD) || (input.EMAILOLD === '')) {
return handleErrorResponse(res, log, false, 500, 'EMAILOLD argument is required');
}
if (!(input.EMAILNEW) || (input.EMAILNEW === '')) {
return handleErrorResponse(res, log, false, 500, 'EMAILNEW argument is required');
}
lists.getByCid(req.params.listId, (err, list) => {
if (err) {
return handleErrorResponse(res, log, err);
}
if (!list) {
return handleErrorResponse(res, log, false, 404, 'Selected listId not found');
}
blacklist.isblacklisted(input.EMAILNEW, (err, blacklisted) => {
if (err) {
return handleErrorResponse(res, log, err);
}
if (blacklisted) {
return handleErrorResponse(res, log, false, 500, 'New email is blacklisted');
}
subscriptions.getByEmail(list.id, input.EMAILOLD, (err, subscription) => {
if (err) {
return handleErrorResponse(res, log, err);
}
if (!subscription) {
return handleErrorResponse(res, log, false, 404, 'Subscription with given old email not found');
}
subscriptions.updateAddressCheck(list, subscription.cid, input.EMAILNEW, null, (err, old, valid) => {
if (err) {
return handleErrorResponse(res, log, err);
}
if (!valid) {
return handleErrorResponse(res, log, false, 500, 'New email not valid');
}
subscriptions.updateAddress(list.id, subscription.id, input.EMAILNEW, (err) => {
if (err) {
return handleErrorResponse(res, log, err);
}
res.status(200);
res.json({
data: {
id: subscription.id,
changedemail: true
}
});
});
});
});
});
});
});
module.exports = router;

View file

@ -126,6 +126,19 @@ router.post('/create', passport.parseForm, passport.csrfProtection, (req, res) =
});
});
router.post('/duplicate', passport.parseForm, passport.csrfProtection, (req, res) => {
campaigns.duplicate(req.body.id, (err, duplicated) => {
if (err) {
req.flash('danger', err && err.message || err);
} else if (duplicated) {
req.flash('success', _('Campaign duplicated'));
} else {
req.flash('info', _('Could not duplicate specified campaign'));
}
return res.redirect('/campaigns/edit/' + duplicated);
});
});
router.get('/edit/:id', passport.csrfProtection, (req, res, next) => {
campaigns.get(req.params.id, false, (err, campaign) => {
if (err || !campaign) {

View 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;

View file

@ -17,6 +17,11 @@
<input type="hidden" name="id" value="{{id}}" />
</form>
<form method="post" class="duplicate-form" id="campaigns-duplicate" action="/campaigns/duplicate">
<input type="hidden" name="_csrf" value="{{csrfToken}}">
<input type="hidden" name="id" value="{{id}}" />
</form>
{{#each attachments}}
<form method="post" id="attachment-download-{{id}}" action="/campaigns/attachment/download">
<input type="hidden" name="_csrf" value="{{../csrfToken}}">
@ -240,6 +245,7 @@
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<div class="pull-right">
<button type="submit" form="campaigns-duplicate" class="btn btn-default"> {{#translate}}Duplicate{{/translate}}</button>
<button type="submit" form="campaigns-delete" class="btn btn-danger"><i class="glyphicon glyphicon-remove"></i> {{#translate}}Delete Campaign{{/translate}}</button>
</div>
<button type="submit" class="btn btn-primary"><i class="glyphicon glyphicon-ok"></i> {{#translate}}Update{{/translate}}</button>

View file

@ -19,6 +19,13 @@
</div>
</div>
<div class="form-group">
<label for="description" class="col-sm-2 control-label">{{#translate}}Field Description{{/translate}}</label>
<div class="col-sm-10">
<textarea class="form-control gpg-text" rows="3" name="description" id="description">{{field.description}}</textarea>
</div>
</div>
<div class="form-group">
<label for="type" class="col-sm-2 control-label">{{#translate}}Field Type{{/translate}}</label>
<div class="col-sm-10">

View file

@ -26,6 +26,13 @@
</div>
</div>
<div class="form-group">
<label for="description" class="col-sm-2 control-label">{{#translate}}Field Description{{/translate}}</label>
<div class="col-sm-10">
<textarea class="form-control gpg-text" rows="3" name="description" id="description">{{field.description}}</textarea>
</div>
</div>
<div class="form-group">
<label for="type" class="col-sm-2 control-label">{{#translate}}Field Type{{/translate}}</label>
<div class="col-sm-10">

View file

@ -36,7 +36,7 @@
{{#translate}}Equals{{/translate}}
</option>
<option value="1" {{#if value.negate}} selected {{/if}}>
{{#translate}}Not squals{{/translate}}
{{#translate}}Not equals{{/translate}}
</option>
</select>
</div>

View file

@ -111,7 +111,7 @@
$.post('/editorapi/update?type={{type}}&editor={{resource.editorName}}', {
id: {{resource.id}},
name: '{{resource.name}}',
{{#if resource.list}} list: {{resource.list}}, {{/if}}
{{#if resource.list}} list: {{resource.list}}+':'+{{resource.segment}}, {{/if}}
html: html,
editorData: JSON.stringify({
template: '{{resource.editorData.template}}',

View file

@ -134,9 +134,12 @@
<label>{{name}}</label>
<input type="hidden" name="origin_{{key}}" value="webform">
{{#each options}}
<p>
<label class="label-checkbox">
<input type="checkbox" name="{{column}}" value="1" {{#if value}} checked {{/if}}> {{name}}
</label>
<span class="help-block">{{description}}</span>
</p>
{{/each}}
</div>
{{/if}}

View file

@ -181,6 +181,7 @@
</p>
<ul>
<li><strong>NAME</strong> {{#translate}}field name{{/translate}} (<em>{{#translate}}required{{/translate}}</em>)</li>
<li><strong>DESCRIPTION</strong> {{#translate}}field description{{/translate}}</li>
<li><strong>TYPE</strong> {{#translate}}one of the following types:{{/translate}}
<ul>
{{#each allowedTypes}}
@ -334,3 +335,32 @@
</p>
<pre>curl -XGET '{{serviceUrl}}api/list/1?access_token={{accessToken}}'</pre>
<h3>POST /api/changeemail/:listId {{#translate}}Change email of existing list subscriber{{/translate}}</h3>
<p>
{{#translate}}This API call changes the email address of an existing list subscriber.{{/translate}}
</p>
<p>
<strong>GET</strong> {{#translate}}arguments{{/translate}}
</p>
<ul>
<li><strong>access_token</strong> {{#translate}}your personal access token{{/translate}}
</ul>
<p>
<strong>POST</strong> {{#translate}}arguments{{/translate}}
</p>
<ul>
<li><strong>EMAILOLD</strong> {{#translate}}subscriber's old email address{{/translate}} (<em>{{#translate}}required{{/translate}}</em>)
<li><strong>EMAILNEW</strong> {{#translate}}subscriber's new email address{{/translate}} (<em>{{#translate}}required{{/translate}}</em>)
</ul>
<p>
<strong>{{#translate}}Example{{/translate}}</strong>
</p>
<pre>curl -XPOST {{serviceUrl}}api/changeemail/B16uVTdW?access_token={{accessToken}} \
--data 'EMAILOLD=test@example.com&EMAILNEW=foo@bar.com'</pre>