Add static Hugo website and mkdocs documentation to docs directory. (#1261)

This commit merges the static website and docs that was on
https://github.com/knadh/listmonk-site repository into the main
listmonk repo.

It also adds a GitHub Actions workflow to public the sites on GitHub
Pages instead of Netlify.
This commit is contained in:
Kailash Nadh 2023-03-26 00:51:20 +05:30 committed by GitHub
parent 152bd37c22
commit 684c15a404
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
69 changed files with 3830 additions and 0 deletions

55
.github/workflows/github-pages.yml vendored Normal file
View file

@ -0,0 +1,55 @@
name: publish-github-pages
on:
push:
branches:
- docs
paths:
- 'docs/**'
workflow_dispatch:
permissions:
contents: write
jobs:
deploy:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v2
with:
submodules: true # Fetch Hugo themes
fetch-depth: 0 # Fetch all history for .GitInfo and .Lastmod
- uses: actions/setup-python@v2
with:
python-version: 3.x
- run: pip install mkdocs-material
- name: Setup Hugo
uses: peaceiris/actions-hugo@v2
with:
hugo-version: '0.68.3'
# Build the main site to the docs/publish directory. This will be the root (/) in gh-pages.
# The -d (output) path is relative to the -s (source) path
- name: Build main site
run: hugo -s docs/site -d ../publish --gc --minify
# Build the mkdocs documentation in the docs/publish/docs dir. This will be at (/docs)
# The -d (output) path is relative to the -f (source) path
- name: Build docs site
run: mkdocs build -f docs/docs/mkdocs.yml -d ../publish/docs
# Copy the static i18n app to the publish directory. This will be at (/i18n)
- name: Copy i18n site
run: cp -R docs/i18n docs/publish
- name: Deploy
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_branch: gh-pages
publish_dir: ./docs/publish
cname: listmonk.app
user_name: 'github-actions[bot]'
user_email: 'github-actions[bot]@users.noreply.github.com'

View file

@ -0,0 +1,56 @@
# APIs
All features that are available on the listmonk dashboard are also available as REST-like HTTP APIs that can be interacted with directly. Request and response bodies are JSON. This allows easy scripting of listmonk and integration with other systems, for instance, synchronisation with external subscriber databases.
API requests require BasicAuth authentication with the admin credentials.
!!! warning "Work in progress"
> The API section is a work in progress. There are a large number of API calls that are yet to be documented.
## Response structure
### Successful request
```http
HTTP/1.1 200 OK
Content-Type: application/json
{
"data": {}
}
```
All responses from the API server are JSON with the content-type application/json unless explicitly stated otherwise. A successful 200 OK response always has a JSON response body with a status key with the value success. The data key contains the full response payload.
### Failed request
```http
HTTP/1.1 500 Server error
Content-Type: application/json
{
"message": "Error message"
}
```
A failure response is preceded by the corresponding 40x or 50x HTTP header. There may be an optional `data` key with additional payload.
### Timestamps
All timestamp fields are in the format `2019-01-01T09:00:00.000000+05:30`. The seconds component is suffixed by the milliseconds, followed by the `+` and the timezone offset.
### Common HTTP error codes
| code |   |
| ----- | ------------------------------------------------------------------------ |
| `400` | Missing or bad request parameters or values |
| `403` | Session expired or invalidate. Must relogin |
| `404` | Request resource was not found |
| `405` | Request method (GET, POST etc.) is not allowed on the requested endpoint |
| `410` | The requested resource is gone permanently |
| `429` | Too many requests to the API (rate limiting) |
| `500` | Something unexpected went wrong |
| `502` | The backend OMS is down and the API is unable to communicate with it |
| `503` | Service unavailable; the API is down |
| `504` | Gateway timeout; the API is unreachable |

View file

@ -0,0 +1,344 @@
# API / Campaigns
Method | Endpoint | Description
-------|---------------------------------------------------------------------------|-----------------------------
`GET` | [/api/campaigns](#get-apicampaigns) | Gets all campaigns.
`GET` | [/api/campaigns/:`campaign_id`](#get-apicampaignscampaign_id) | Gets a single campaign.
`GET` | [/api/campaigns/:`campaign_id`/preview](#get-apicampaignscampaign_idpreview) | Gets the HTML preview of a campaign body.
`GET` | [/api/campaigns/running/stats](#get-apicampaignsrunningstats) | Gets the stats of a given set of campaigns.
`POST` | [/api/campaigns](#post-apicampaigns) | Creates a new campaign.
`POST` | /api/campaigns/:`campaign_id`/test | Posts campaign message to arbitrary subscribers for testing.
`PUT` | /api/campaigns/:`campaign_id` | Modifies a campaign.
`PUT` | [/api/campaigns/:`campaign_id`/status](#put-apicampaignscampaign_idstatus) | Start / pause / cancel / schedule a campaign.
`DELETE` | [/api/campaigns/:`campaign_id`](#delete-apicampaignscampaign_id) | Deletes a campaign.
#### ```GET``` /api/campaigns
Gets all campaigns.
##### Example Request
```shell
curl -u "username:password" -X GET 'http://localhost:9000/api/campaigns?page=1&per_page=100'
```
##### Parameters
Name | Type | Required/Optional | Description
--------|--------------------|-------------|---------------------|---------------------
`query` | string | Optional | Optional string to search a list by name.
`order_by` | string | Optional | Field to sort results by. `name|status|created_at|updated_at`
`order` | string | Optional | `ASC|DESC`Sort by ascending or descending order.
`page` | number | Optional | Page number for paginated results.
`per_page` | number | Optional | Results to return per page. Setting this to `all` skips pagination and returns all results.
##### Example Response
``` json
{
"data": {
"results": [
{
"id": 1,
"created_at": "2020-03-14T17:36:41.29451+01:00",
"updated_at": "2020-03-14T17:36:41.29451+01:00",
"CampaignID": 0,
"views": 0,
"clicks": 0,
"lists": [
{
"id": 1,
"name": "Default list"
}
],
"started_at": null,
"to_send": 0,
"sent": 0,
"uuid": "57702beb-6fae-4355-a324-c2fd5b59a549",
"type": "regular",
"name": "Test campaign",
"subject": "Welcome to listmonk",
"from_email": "No Reply <noreply@yoursite.com>",
"body": "<h3>Hi {{ .Subscriber.FirstName }}!</h3>\n\t\t\tThis is a test e-mail campaign. Your second name is {{ .Subscriber.LastName }} and you are from {{ .Subscriber.Attribs.city }}.",
"send_at": "2020-03-15T17:36:41.293233+01:00",
"status": "draft",
"content_type": "richtext",
"tags": [
"test-campaign"
],
"template_id": 1,
"messenger": "email"
}
],
"query": "",
"total": 1,
"per_page": 20,
"page": 1
}
}
```
#### ```GET``` /api/campaigns/:`campaign_id`
Gets a single campaign.
##### Parameters
Name | Parameter Type | Data Type | Required/Optional | Description
------------|-------------------|--------------|----------------------|-----------------------------
`campaign_id` | Path Parameter | Number | Required | The id value of the campaign you want to get.
##### Example Request
``` shell
curl -u "username:password" -X GET 'http://localhost:9000/api/campaigns/1'
```
##### Example Response
``` json
{
"data": {
"id": 1,
"created_at": "2020-03-14T17:36:41.29451+01:00",
"updated_at": "2020-03-14T17:36:41.29451+01:00",
"CampaignID": 0,
"views": 0,
"clicks": 0,
"lists": [
{
"id": 1,
"name": "Default list"
}
],
"started_at": null,
"to_send": 0,
"sent": 0,
"uuid": "57702beb-6fae-4355-a324-c2fd5b59a549",
"type": "regular",
"name": "Test campaign",
"subject": "Welcome to listmonk",
"from_email": "No Reply <noreply@yoursite.com>",
"body": "<h3>Hi {{ .Subscriber.FirstName }}!</h3>\n\t\t\tThis is a test e-mail campaign. Your second name is {{ .Subscriber.LastName }} and you are from {{ .Subscriber.Attribs.city }}.",
"send_at": "2020-03-15T17:36:41.293233+01:00",
"status": "draft",
"content_type": "richtext",
"tags": [
"test-campaign"
],
"template_id": 1,
"messenger": "email"
}
}
```
#### ```GET``` /api/campaigns/:`campaign_id`/preview
Gets the html preview of a campaign body.
##### Parameters
Name | Parameter Type | Data Type | Required/Optional | Description
------------|-------------------|--------------|----------------------|-----------------------------
`campaign_id` | Path Parameter | Number | Required | The id value of the campaign to be previewed.
##### Example Request
```shell
curl -u "username:password" -X GET 'http://localhost:9000/api/campaigns/1/preview'
```
##### Example Response
``` html
<h3>Hi John!</h3>
This is a test e-mail campaign. Your second name is Doe and you are from Bengaluru.
```
#### ```GET``` /api/campaigns/running/stats
Gets the running stat of a given set of campaigns.
##### Parameters
Name | Parameter Type | Data Type | Required/Optional | Description
------------|------------------|------------|---------------------|--------------------------------
campaign_id | Query Parameters | Number | Required | The id values of the campaigns whose stat you want to get.
##### Example Request
``` shell
curl -u "username:password" -X GET 'http://localhost:9000/api/campaigns/running/stats?campaign_id=1'
```
##### Example Response
``` json
{
"data": []
}
```
### ```POST ``` /api/campaigns
Creates a new campaign.
#### Parameters
| Name | Data type | Required/Optional | Description |
|----------------|-----------|-------------------|--------------------------------------------------------------------------------------------------------|
| `name` | String | Required | Name of the campaign. |
| `subject` | String | Required | (E-mail) subject of the campaign. |
| `lists` | []Number | Required | Array of list IDs to send the campaign to. |
| `from_email` | String | Optional | `From` e-mail to show on the campaign e-mails. If left empty, the default value from settings is used. |
| `type` | String | Required | `regular` or `optin` campaign. |
| `content_type` | String | Required | `richtext`, `html`, `markdown`, `plain` |
| `body` | String | Required | Campaign content body. |
| `altbody` | String | Optional | Alternate plain text body for HTML (and richtext) e-mails. |
| `send_at` | String | Optional | A timestamp to schedule the campaign at. Eg: `2021-12-25T06:00:00` (YYYY-MM-DDTHH:MM:SS) |
| `messenger` | String | Optional | `email` or a custom messenger defined in the settings. If left empty, `email` is used. |
| `template_id` | Number | Optional | ID of the template to use. If left empty, the default template is used. |
| `tags` | []String | Optional | Array of string tags to mark the campaign. |
#### Example request
```shell
curl -u "username:password" 'http://localhost:9000/api/campaigns' -X POST -H 'Content-Type: application/json;charset=utf-8' --data-raw '{"name":"Test campaign","subject":"Hello, world","lists":[1],"from_email":"listmonk <noreply@listmonk.yoursite.com>","content_type":"richtext","messenger":"email","type":"regular","tags":["test"],"template_id":1}'
```
#### Example response
```json
{
"data": {
"id": 1,
"created_at": "2021-12-27T11:50:23.333485Z",
"updated_at": "2021-12-27T11:50:23.333485Z",
"views": 0,
"clicks": 0,
"bounces": 0,
"lists": [{
"id": 1,
"name": "Default list"
}],
"started_at": null,
"to_send": 1,
"sent": 0,
"uuid": "90c889cc-3728-4064-bbcb-5c1c446633b3",
"type": "regular",
"name": "Test campaign",
"subject": "Hello, world",
"from_email": "listmonk \u003cnoreply@listmonk.yoursite.com\u003e",
"body": "",
"altbody": null,
"send_at": null,
"status": "draft",
"content_type": "richtext",
"tags": ["test"],
"template_id": 1,
"messenger": "email"
}
}
```
#### ```PUT``` /api/campaigns/:`campaign_id`/status
Modifies a campaign status to start, pause, cancel, or schedule a campaign.
##### Parameters
Name | Parameter Type | Data Type | Required/Optional | Description
------------------|-------------------------|---------------------------|----------------------|-----------------------------
`campaign_id` | Path Parameter | Number | Required | The id value of the campaign whose status is to be modified.
`status` | Request Body | String | Required | `scheduled`, `running`, `paused`, `cancelled`.
###### Note:
> * Only "scheduled" campaigns can be saved as "draft".
* Only "draft" campaigns can be "scheduled".
* Only "paused" campaigns and "draft" campaigns can be started.
* Only "running" campaigns can be "cancelled" and "paused".
##### Example Request
```shell
curl -u "username:password" -X PUT 'http://localhost:9000/api/campaigns/1/status' \
--header 'Content-Type: application/json' \
--data-raw '{"status":"scheduled"}'
```
##### Example Response
```json
{
"data": {
"id": 1,
"created_at": "2020-03-14T17:36:41.29451+01:00",
"updated_at": "2020-04-08T19:35:17.331867+01:00",
"CampaignID": 0,
"views": 0,
"clicks": 0,
"lists": [
{
"id": 1,
"name": "Default list"
}
],
"started_at": null,
"to_send": 0,
"sent": 0,
"uuid": "57702beb-6fae-4355-a324-c2fd5b59a549",
"type": "regular",
"name": "Test campaign",
"subject": "Welcome to listmonk",
"from_email": "No Reply <noreply@yoursite.com>",
"body": "<h3>Hi {{ .Subscriber.FirstName }}!</h3>\n\t\t\tThis is a test e-mail campaign. Your second name is {{ .Subscriber.LastName }} and you are from {{ .Subscriber.Attribs.city }}.",
"send_at": "2020-03-15T17:36:41.293233+01:00",
"status": "scheduled",
"content_type": "richtext",
"tags": [
"test-campaign"
],
"template_id": 1,
"messenger": "email"
}
}
```
#### ```DELETE``` /api/campaigns/:`campaign_id`
Deletes a campaign, only scheduled campaigns that have not yet been started can be deleted.
##### Parameters
Name | Parameter Type | Data Type | Required/Optional | Description
---------|--------------------|----------------|---------------------|------------------------------
`campaign_id`| Path Parameter | Number | Required | The id value of the campaign you want to delete.
##### Example Request
```shell
curl -u "username:password" -X DELETE 'http://localhost:9000/api/campaigns/34'
```
##### Example Response
```json
{
"data": true
}
```

View file

@ -0,0 +1,89 @@
# API / Import
Method | Endpoint | Description
-----------|----------------------------------------------------------------------|--------------
`GET` | [api/import/subscribers](#get-apiimportsubscribers) | Gets a import statistics.
`GET` | [api/import/subscribers/logs](#get-apiimportsubscriberslogs) | Get a import statistics .
`POST` | [api/import/subscribers](#post-apiimportsubscribers) | Upload a ZIP file or CSV file to bulk import subscribers.
`DELETE` | [api/import/subscribers](#delete-apiimportsubscribers) | Stops and deletes a import.
#### **`GET`** api/import/subscribers
Gets import status.
##### Example Request
```shell
curl -u "username:username" -X GET 'http://localhost:9000/api/import/subscribers'
```
##### Example Response
```json
{
"data": {
"name": "",
"total": 0,
"imported": 0,
"status": "none"
}
}
```
#### **`GET`** api/import/subscribers/logs
Gets import logs.
##### Example Request
```shell
curl -u "username:username" -X GET 'http://localhost:9000/api/import/subscribers/logs'
```
##### Example Response
```json
{
"data": "2020/04/08 21:55:20 processing 'import.csv'\n2020/04/08 21:55:21 imported finished\n"
}
```
#### **`POST`** api/import/subscribers
Post a CSV (optionally zipped) file to do a bulk import. The request should be a multipart form POST.
##### Parameters
Name | Parameter type | Data type | Required/Optional | Description
---------|----------------|----------------|-------------------|-----------------------
`params` | Request body | String | Required | Stringified JSON with import params
`file` | Request body | File | Required | File to upload
***params*** (JSON string)
```json
{
"mode": "subscribe", // subscribe or blocklist
"delim": ",", // delimiter in the uploaded file
"lists":[1], // array of list IDs to import into
"overwrite": true // overwrite existing entries or skip them?
}
```
#### **`DELETE`** api/import/subscribers
Stops and deletes an import.
##### Example Request
```shell
curl -u "username:username" -X DELETE 'http://localhost:9000/api/import/subscribers'
```
##### Example Response
```json
{
"data": {
"name": "",
"total": 0,
"imported": 0,
"status": "none"
}
}
```

View file

@ -0,0 +1,162 @@
# API / Lists
Method | Endpoint | Description
------------|------------------------------------------------------|----------------------------------------------
`GET` | [/api/lists](#get-apilists) | Gets all lists.
`GET` | [/api/lists/:`list_id`](#get-apilistslist_id) | Gets a single list.
`POST` | [/api/lists](#post-apilists) | Creates a new list.
`PUT` | /api/lists/:`list_id` | Modifies a list.
`DELETE` | [/api/lists/:`list_id`](#put-apilistslist_id) | Deletes a list.
#### **`GET`** /api/lists
Gets lists.
##### Parameters
Name | Type | Required/Optional | Description
-----------|--------|--------------------|-----------------------------------------
`query` | string | Optional | Optional string to search a list by name.
`order_by` | string | Optional | Field to sort results by. `name|status|created_at|updated_at`
`order` | string | Optional | `ASC|DESC`Sort by ascending or descending order.
`page` | number | Optional | Page number for paginated results.
`per_page` | number | Optional | Results to return per page. Setting this to `all` skips pagination and returns all results.
##### Example Request
```shell
curl -u "username:username" -X GET 'http://localhost:9000/api/lists?page=1&per_page=100'
```
##### Example Response
```json
{
"data": {
"results": [
{
"id": 1,
"created_at": "2020-02-10T23:07:16.194843+01:00",
"updated_at": "2020-03-06T22:32:01.118327+01:00",
"uuid": "ce13e971-c2ed-4069-bd0c-240e9a9f56f9",
"name": "Default list",
"type": "public",
"optin": "double",
"tags": [
"test"
],
"subscriber_count": 2
},
{
"id": 2,
"created_at": "2020-03-04T21:12:09.555013+01:00",
"updated_at": "2020-03-06T22:34:46.405031+01:00",
"uuid": "f20a2308-dfb5-4420-a56d-ecf0618a102d",
"name": "get",
"type": "private",
"optin": "single",
"tags": [],
"subscriber_count": 0
}
],
"total": 5,
"per_page": 20,
"page": 1
}
}
```
#### **`GET`** /api/lists/:`list_id`
Gets a single list.
##### Parameters
Name | Parameter type | Data type | Required/Optional | Description
----------|--------------------|-------------|---------------------|---------------------
`list_id` | Path parameter | number | Required | The id value of the list you want to get.
##### Example Request
``` shell
curl -u "username:username" -X GET 'http://localhost:9000/api/lists/5'
```
##### Example Response
```json
{
"data": {
"id": 5,
"created_at": "2020-03-07T06:31:06.072483+01:00",
"updated_at": "2020-03-07T06:31:06.072483+01:00",
"uuid": "1bb246ab-7417-4cef-bddc-8fc8fc941d3a",
"name": "Test list",
"type": "public",
"optin": "double",
"tags": [],
"subscriber_count": 0
}
}
```
#### **`POST`** /api/lists
Creates a new list.
##### Parameters
Name | Parameter type | Data type | Required/Optional | Description
--------|-----------------|-------------|--------------------|----------------
name | Request body | string | Required | The new list name.
type | Request body | string | Required | List type, can be set to `private` or `public`.
optin | Request body | string | Required | `single` or `double` optin.
tags | Request body | string[] | Optional | The tags associated with the list.
##### Example Request
``` shell
curl -u "username:username" -X POST 'http://localhost:9000/api/lists'
```
##### Example Response
```json
{
"data": {
"id": 5,
"created_at": "2020-03-07T06:31:06.072483+01:00",
"updated_at": "2020-03-07T06:31:06.072483+01:00",
"uuid": "1bb246ab-7417-4cef-bddc-8fc8fc941d3a",
"name": "Test list",
"type": "public",
"tags": [],
"subscriber_count": 0
}
}
null
```
#### **`PUT`** /api/lists/`list_id`
Modifies a list.
##### Parameters
Name | Parameter type | Data type | Required/Optional | Description
----------|--------------------|--------------|-----------------------|-------------------------
`list_id` | Path parameter | number | Required | The id of the list to be modified.
name | Request body | string | Optional | The name which the old name will be modified to.
type | Request body | string | Optional | List type, can be set to `private` or `public`.
optin | Request body | string | Optional | `single` or `double` optin.
tags | Request body | string[] | Optional | The tags associated with the list.
##### Example Request
```shell
curl -u "username:username" -X PUT 'http://localhost:9000/api/lists/5' \
--form 'name=modified test list' \
--form 'type=private'
```
##### Example Response
``` json
{
"data": {
"id": 5,
"created_at": "2020-03-07T06:31:06.072483+01:00",
"updated_at": "2020-03-07T06:52:15.208075+01:00",
"uuid": "1bb246ab-7417-4cef-bddc-8fc8fc941d3a",
"name": "modified test list",
"type": "private",
"optin": "single",
"tags": [],
"subscriber_count": 0
}
}
```

View file

@ -0,0 +1,107 @@
# API / Media
Method | Endpoint | Description
--------------|--------------------------------------------------------------|----------------------------------------------
`GET` | [/api/media](#get-apimedia) | Gets an uploaded media file.
`POST` | [/api/media](#post-apimedia) | Uploads a media file.
`DELETE` | [/api/media/:`media_id`](#delete-apimediamedia_id) | Deletes uploaded media files.
#### **`GET`** /api/media
Gets an uploaded media file.
##### Example Request
```shell
curl -u "username:username" -X GET 'http://localhost:9000/api/media' \
--header 'Content-Type: multipart/form-data; boundary=--------------------------093715978792575906250298'
```
##### Example Response
```json
{
"data": [
{
"id": 1,
"uuid": "ec7b45ce-1408-4e5c-924e-965326a20287",
"filename": "Media file",
"created_at": "2020-04-08T22:43:45.080058+01:00",
"thumb_uri": "/uploads/image_thumb.jpg",
"uri": "/uploads/image.jpg"
}
]
}
```
Response definitions
The following table describes each item in the response.
|Response item |Description |Data type |
|:---------------:|:------------|:----------:|
|data|Array of the media file objects, which contains an information about the uploaded media files|array|
|id|Media file object ID|number (int)|
|uuid|Media file uuuid|string (uuid)|
|filename|Name of the media file|string|
|created_at|Date and time, when the media file object was created|String (localDateTime)|
|thumb_uri|The thumbnail URI of the media file|string|
|uri|URI of the media file|string|
#### **`POST`** /api/media
Uploads a media file.
##### Parameters
Nam | Parameter Type | Data Type | Required/Optional | Description
-----------|-----------------------|-------------------|-------------------------|---------------------------------
file | Request body | Media file | Required | The media file to be uploaded.
##### Example Request
```shell
curl -u "username:username" -X POST 'http://localhost:9000/api/media' \
--header 'Content-Type: multipart/form-data; boundary=--------------------------183679989870526937212428' \
--form 'file=@/path/to/image.jpg'
```
##### Example Response
``` json
{
"data": {
"id": 1,
"uuid": "ec7b45ce-1408-4e5c-924e-965326a20287",
"filename": "Media file",
"created_at": "2020-04-08T22:43:45.080058+01:00",
"thumb_uri": "/uploads/image_thumb.jpg",
"uri": "/uploads/image.jpg"
}
}
```
Response definitions
|Response item |Description |Data type |
|:---------------:|:------------:|:----------:|
|data|True means that the media file was successfully uploaded |boolean|
#### **`DELETE`** /api/media/:`media_id`
Deletes an uploaded media file.
##### Parameters
Name | Parameter Type | Data Type | Required/Optional | Description
----------------|-------------------------|--------------------|-------------------------|--------------------------
`Media_id` | Path Parameter | Number | Required | The id of the media file you want to delete.
##### Example Request
```shell
curl -u "username:username" -X DELETE 'http://localhost:9000/api/media/1'
```
##### Example Response
```json
{
"data": true
}
```
Response definitions
|Response item |Description |Data type |
|:---------------:|:------------:|:----------:|
|data|True means that the media file was successfully deleted |boolean|

View file

@ -0,0 +1,461 @@
# API / Subscribers
Method | Endpoint | Description
---------|-----------------------------------------------------------------------|-----------------------------------------------------------
`GET` | [/api/subscribers](#get-apisubscribers) | Gets all subscribers.
`GET` | [/api/subscribers/:`id`](#get-apisubscribersid) | Gets a single subscriber.
`GET` | /api/subscribers/lists/:`id` | Gets subscribers in a list.
`GET` | [/api/subscribers](#get-apisubscriberslist_id) | Gets subscribers in one or more lists.
`GET` | [/api/subscribers](#get-apisubscribers_1) | Gets subscribers filtered by an arbitrary SQL expression.
`POST` | [/api/subscribers](#post-apisubscribers) | Creates a new subscriber.
`PUT` | [/api/subscribers/lists](#put-apisubscriberslists) | Modify subscribers' list memberships.
`PUT` | /api/subscribers/:`id` | Updates a subscriber by ID.
`PUT` | [/api/subscribers/:`id`/blocklist](#put-apisubscribersidblocklist) | Blocklists a single subscriber.
`PUT` | /api/subscribers/blocklist | Blocklists one or more subscribers.
`PUT` | [/api/subscribers/query/blocklist](#put-apisubscribersqueryblocklist) | Blocklists subscribers with an arbitrary SQL expression.
`DELETE` | [/api/subscribers/:`id`](#delete-apisubscribersid) | Deletes a single subscriber.
`DELETE` | [/api/subscribers](#delete-apisubscribers) | Deletes one or more subscribers .
`POST` | [/api/subscribers/query/delete](#post-apisubscribersquerydelete) | Deletes subscribers with an arbitrary SQL expression.
#### **`GET`** /api/subscribers
Gets all subscribers.
##### Example Request
```shell
curl 'http://localhost:9000/api/subscribers?page=1&per_page=100'
```
To skip pagination and retrieve all records, pass `per_page=all`.
##### Example Response
```json
{
"data": {
"results": [
{
"id": 1,
"created_at": "2020-02-10T23:07:16.199433+01:00",
"updated_at": "2020-02-10T23:07:16.199433+01:00",
"uuid": "ea06b2e7-4b08-4697-bcfc-2a5c6dde8f1c",
"email": "john@example.com",
"name": "John Doe",
"attribs": {
"city": "Bengaluru",
"good": true,
"type": "known"
},
"status": "enabled",
"lists": [
{
"subscription_status": "unconfirmed",
"id": 1,
"uuid": "ce13e971-c2ed-4069-bd0c-240e9a9f56f9",
"name": "Default list",
"type": "public",
"tags": [
"test"
],
"created_at": "2020-02-10T23:07:16.194843+01:00",
"updated_at": "2020-02-10T23:07:16.194843+01:00"
}
]
},
{
"id": 2,
"created_at": "2020-02-18T21:10:17.218979+01:00",
"updated_at": "2020-02-18T21:10:17.218979+01:00",
"uuid": "ccf66172-f87f-4509-b7af-e8716f739860",
"email": "quadri@example.com",
"name": "quadri",
"attribs": {},
"status": "enabled",
"lists": [
{
"subscription_status": "unconfirmed",
"id": 1,
"uuid": "ce13e971-c2ed-4069-bd0c-240e9a9f56f9",
"name": "Default list",
"type": "public",
"tags": [
"test"
],
"created_at": "2020-02-10T23:07:16.194843+01:00",
"updated_at": "2020-02-10T23:07:16.194843+01:00"
}
]
},
{
"id": 3,
"created_at": "2020-02-19T19:10:49.36636+01:00",
"updated_at": "2020-02-19T19:10:49.36636+01:00",
"uuid": "5d940585-3cc8-4add-b9c5-76efba3c6edd",
"email": "sugar@example.com",
"name": "sugar",
"attribs": {},
"status": "enabled",
"lists": []
}
],
"query": "",
"total": 3,
"per_page": 20,
"page": 1
}
}
```
#### **`GET`** /api/subscribers/:`id`
Gets a single subscriber.
##### Parameters
Name | Parameter type |Data type | Required/Optional | Description
---------|----------------|----------------|-------------------|-----------------------
`id` | Path parameter | Number | Required | The id value of the subscriber you want to get.
##### Example Request
```shell
curl 'http://localhost:9000/api/subscribers/1'
```
##### Example Response
```json
{
"data": {
"id": 1,
"created_at": "2020-02-10T23:07:16.199433+01:00",
"updated_at": "2020-02-10T23:07:16.199433+01:00",
"uuid": "ea06b2e7-4b08-4697-bcfc-2a5c6dde8f1c",
"email": "john@example.com",
"name": "John Doe",
"attribs": {
"city": "Bengaluru",
"good": true,
"type": "known"
},
"status": "enabled",
"lists": [
{
"subscription_status": "unconfirmed",
"id": 1,
"uuid": "ce13e971-c2ed-4069-bd0c-240e9a9f56f9",
"name": "Default list",
"type": "public",
"tags": [
"test"
],
"created_at": "2020-02-10T23:07:16.194843+01:00",
"updated_at": "2020-02-10T23:07:16.194843+01:00"
}
]
}
}
```
#### **`GET`** /api/subscribers
Gets subscribers in one or more lists.
##### Parameters
Name | Parameter type | Data type | Required/Optional | Description
----------|-----------------|-------------|---------------------|---------------
`List_id` | Request body | Number | Required | ID of the list to fetch subscribers from.
##### Example Request
```shell
curl 'http://localhost:9000/api/subscribers?list_id=1&list_id=2&page=1&per_page=100'
```
To skip pagination and retrieve all records, pass `per_page=all`.
##### Example Response
```json
{
"data": {
"results": [
{
"id": 1,
"created_at": "2019-06-26T16:51:54.37065+05:30",
"updated_at": "2019-07-03T11:53:53.839692+05:30",
"uuid": "5e91dda1-1c16-467d-9bf9-2a21bf22ae21",
"email": "test@test.com",
"name": "Test Subscriber",
"attribs": {
"city": "Bengaluru",
"projects": 3,
"stack": {
"languages": ["go", "python"]
}
},
"status": "enabled",
"lists": [
{
"subscription_status": "unconfirmed",
"id": 1,
"uuid": "41badaf2-7905-4116-8eac-e8817c6613e4",
"name": "Default list",
"type": "public",
"tags": ["test"],
"created_at": "2019-06-26T16:51:54.367719+05:30",
"updated_at": "2019-06-26T16:51:54.367719+05:30"
}
]
}
],
"query": "",
"total": 1,
"per_page": 20,
"page": 1
}
}
```
#### **`GET`** /api/subscribers
Gets subscribers with an SQL expression.
##### Example Request
```shell
curl -X GET 'http://localhost:9000/api/subscribers' \
--url-query 'page=1' \
--url-query 'per_page=100' \
--url-query "query=subscribers.name LIKE 'Test%' AND subscribers.attribs->>'city' = 'Bengaluru'"
```
To skip pagination and retrieve all records, pass `per_page=all`.
>Refer to the [querying and segmentation](/docs/querying-and-segmentation#querying-and-segmenting-subscribers) section for more information on how to query subscribers with SQL expressions.
##### Example Response
```json
{
"data": {
"results": [
{
"id": 1,
"created_at": "2019-06-26T16:51:54.37065+05:30",
"updated_at": "2019-07-03T11:53:53.839692+05:30",
"uuid": "5e91dda1-1c16-467d-9bf9-2a21bf22ae21",
"email": "test@test.com",
"name": "Test Subscriber",
"attribs": {
"city": "Bengaluru",
"projects": 3,
"stack": {
"frameworks": ["echo", "go"],
"languages": ["go", "python"]
}
},
"status": "enabled",
"lists": [
{
"subscription_status": "unconfirmed",
"id": 1,
"uuid": "41badaf2-7905-4116-8eac-e8817c6613e4",
"name": "Default list",
"type": "public",
"tags": ["test"],
"created_at": "2019-06-26T16:51:54.367719+05:30",
"updated_at": "2019-06-26T16:51:54.367719+05:30"
}
]
}
],
"query": "subscribers.name LIKE 'Test%' AND subscribers.attribs-\u003e\u003e'city' = 'Bengaluru'",
"total": 1,
"per_page": 20,
"page": 1
}
}
```
#### **`POST`** /api/subscribers
Creates a new subscriber.
##### Parameters
Name | Parameter type | Data type | Required/Optional | Description
-------------------------|------------------|------------|-------------------|----------------------------
email | Request body | String | Required | The email address of the new susbcriber.
name | Request body | String | Required | The name of the new subscriber.
status | Request body | String | Required | The status of the new subscriber. Can be enabled, disabled or blocklisted.
lists | Request body | Numbers | Optional | Array of list IDs to subscribe to (marked as `unconfirmed` by default).
attribs | Request body | json | Optional | JSON list containing new subscriber's attributes.
preconfirm_subscriptions | Request body | Bool | Optional | If `true`, marks subscriptsions as `confirmed` and no-optin e-mails are sent for double opt-in lists.
##### Example Request
```shell
curl 'http://localhost:9000/api/subscribers' -H 'Content-Type: application/json' \
--data '{"email":"subsriber@domain.com","name":"The Subscriber","status":"enabled","lists":[1],"attribs":{"city":"Bengaluru","projects":3,"stack":{"languages":["go","python"]}}}'
```
##### Example Response
```json
{
"data": {
"id": 3,
"created_at": "2019-07-03T12:17:29.735507+05:30",
"updated_at": "2019-07-03T12:17:29.735507+05:30",
"uuid": "eb420c55-4cfb-4972-92ba-c93c34ba475d",
"email": "subsriber@domain.com",
"name": "The Subscriber",
"attribs": {
"city": "Bengaluru",
"projects": 3,
"stack": { "languages": ["go", "python"] }
},
"status": "enabled",
"lists": [1]
}
}
```
#### **`PUT`** /api/subscribers/lists
Modify subscribers list memberships.
##### Parameters
Name | Paramter type | Data type | Required/Optional | Description
------------------|---------------|-----------|--------------------|-------------------------------------------------------
`ids` | Request body | Numbers | Required | The ids of the subscribers to be modified.
`action` | Request body | String | Required | Wether to `add`, `remove`, or `unsubscribe` the users.
`target_list_ids` | Request body | Numbers | Required | The ids of the lists to be modified.
`status` | Request body | String | Required for `add` | `confirmed`, `unconfirmed`, or `unsubscribed` status.
##### Example Request
To subscribe users 1, 2, and 3 to lists 4, 5, and 6:
```shell
curl -u "username:username" -X PUT 'http://localhost:9000/api/subscribers/lists' \
--data-raw '{"ids": [1, 2, 3], "action": "add", "target_list_ids": [4, 5, 6], "status": "confirmed"}'
```
##### Example Response
``` json
{
"data": true
}
```
#### **`PUT`** /api/subscribers/:`id`/blocklist
Blocklists a single subscriber.
##### Parameters
Name | Parameter type | Data type | Required/Optional | Description
------|----------------|------------|-------------------|-------------
`id` | Path parameter | Number | Required | The id value of the subscriber you want to blocklist.
##### Example Request
```shell
curl -u "username:username" -X PUT 'http://localhost:9000/api/subscribers/9/blocklist'
```
##### Example Response
``` json
{
"data": true
}
```
#### **`PUT`** /api/subscribers/query/blocklist
Blocklists subscribers with an arbitrary sql expression.
##### Example Request
``` shell
curl -u "username:username" -X PUT 'http://localhost:9000/api/subscribers/query/blocklist' \
--data-raw '"query=subscribers.name LIKE '\''John Doe'\'' AND subscribers.attribs->>'\''city'\'' = '\''Bengaluru'\''"'
```
>Refer to the [querying and segmentation](/querying-and-segmentation#querying-and-segmenting-subscribers) section for more information on how to query subscribers with SQL expressions.
##### Example Response
``` json
{
"data": true
}
```
#### **`DELETE`** /api/subscribers/:`id`
Deletes a single subscriber.
##### Parameters
Name | Parameter type | Data type | Required/Optional | Description
--------|------------------|-------------|--------------------|------------------
`id` | Path parameter | Number | Required | The id of the subscriber you want to delete.
##### Example Request
``` shell
curl -u "username:username" -X DELETE 'http://localhost:9000/api/subscribers/9'
```
##### Example Response
``` json
{
"data": true
}
```
#### **`DELETE`** /api/subscribers
Deletes one or more subscribers.
##### Parameters
Name | Parameter type | Data type | Required/Optional | Description
--------|---------------------|----------------|-----------------------|--------------
id | Query parameters | Number | Required | The id of the subscribers you want to delete.
##### Example Request
``` shell
curl -u "username:username" -X DELETE 'http://localhost:9000/api/subscribers?id=10&id=11'
```
##### Example Response
``` json
{
"data": true
}
```
#### **`POST`** /api/subscribers/query/delete
Deletes subscribers with an arbitrary SQL expression.
##### Example Request
``` shell
curl -u "username:username" -X POST 'http://localhost:9000/api/subscribers/query/delete' \
--data-raw '"query=subscribers.name LIKE '\''John Doe'\'' AND subscribers.attribs->>'\''city'\'' = '\''Bengaluru'\''"'
```
>Refer to the [querying and segmentation](/querying-and-segmentation#querying-and-segmenting-subscribers) section for more information on how to query subscribers with SQL expressions.
##### Example Response
``` json
{
"data": true
}
```

View file

@ -0,0 +1,145 @@
# API / Templates
Method | Endpoint | Description
---------------------|-----------------------------------------|-----------------------------------------------------
`GET` | [/api/templates](#get-apitemplates) | Gets all templates.
`GET` | [/api/templates/:`template_id`](#get-apitemplatestemplate_id) | Gets a single template.
`GET` | [/api/templates/:`template_id`/preview](#get-apitemplatestemplate_idpreview) | Gets the HTML preview of a template.
`POST` | /api/templates/preview |
`POST` | /api/templates | Creates a template.
`PUT` | /api/templates/:`template_id` | Modifies a template.
`PUT` | [/api/templates/:`template_id`/default](#put-apitemplatestemplate_iddefault) | Sets a template to the default template.
`DELETE` | [/api/templates/:`template_id`](#delete-apitemplatestemplate_id) | Deletes a template.
#### **`GET`** /api/templates
Gets all templates.
##### Example Request
```shell
curl -u "username:username" -X GET 'http://localhost:9000/api/templates'
```
##### Example Response
``` json
{
"data": [
{
"id": 1,
"created_at": "2020-03-14T17:36:41.288578+01:00",
"updated_at": "2020-03-14T17:36:41.288578+01:00",
"name": "Default template",
"body": "{{ template \"content\" . }}",
"type": "campaign",
"is_default": true
}
]
}
```
#### **`GET`** /api/templates/:`template_id`
Gets a single template.
##### Parameters
Name | Parameter Type | Data Type | Required/Optional | Description
------------|------------------------|---------------------|-------------------------|------------------------------------------
`template_id` | Path Parameter | Number | Required | The id value of the template you want to get.
##### Example Request
``` shell
curl -u "username:username" -X GET 'http://localhost:9000/api/templates/1'
```
##### Example Response
```json
{
"data": {
"id": 1,
"created_at": "2020-03-14T17:36:41.288578+01:00",
"updated_at": "2020-03-14T17:36:41.288578+01:00",
"name": "Default template",
"body": "{{ template \"content\" . }}",
"type": "campaign",
"is_default": true
}
}
```
#### **`GET`** /api/templates/:`template_id`/preview
Gets the HTML preview of a template body.
##### Parameters
Name | Parameter Type | Data Type | Required/Optional | Description
------------|----------------------|-----------------|------------------------|---------------------------------
`template_id` | Path Parameter | Number | Required | The id value of the template whose html preview you want to get.
##### Example Request
``` shell
curl -u "username:username" -X GET 'http://localhost:9000/api/templates/1/preview'
```
##### Example Response
``` html
<p>Hi there</p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis et elit ac elit sollicitudin condimentum non a magna.
Sed tempor mauris in facilisis vehicula. Aenean nisl urna, accumsan ac tincidunt vitae, interdum cursus massa.
Interdum et malesuada fames ac ante ipsum primis in faucibus. Aliquam varius turpis et turpis lacinia placerat.
Aenean id ligula a orci lacinia blandit at eu felis. Phasellus vel lobortis lacus. Suspendisse leo elit, luctus sed
erat ut, venenatis fermentum ipsum. Donec bibendum neque quis.</p>
<h3>Sub heading</h3>
<p>Nam luctus dui non placerat mattis. Morbi non accumsan orci, vel interdum urna. Duis faucibus id nunc ut euismod.
Curabitur et eros id erat feugiat fringilla in eget neque. Aliquam accumsan cursus eros sed faucibus.</p>
<p>Here is a link to <a href="https://listmonk.app" target="_blank">listmonk</a>.</p>
```
#### **`PUT`** /api/templates/:`template_id`/default
Sets a template to the default template.
##### Parameters
Name | Parameter Type | Data Type | Required/Optional | Description
------------|------------------------|---------------------|-------------------------|------------------------------------------
`template_id` | Path Parameter | Number | Required | The id value of the template you want to set to the default template.
##### Example Request
``` shell
curl -u "username:username" -X PUT 'http://localhost:9000/api/templates/1/default'
```
##### Example Response
``` json
{
"data": {
"id": 1,
"created_at": "2020-03-14T17:36:41.288578+01:00",
"updated_at": "2020-03-14T17:36:41.288578+01:00",
"name": "Default template",
"body": "{{ template \"content\" . }}",
"type": "campaign",
"is_default": true
}
}
```
#### **`DELETE`** /api/templates/:`template_id`
Deletes a template.
##### Parameters
Name | Parameter Type | Data Type | Required/Optional | Description
------------|------------------------|---------------------|-------------------------|------------------------------------------
`template_id` | Path Parameter | Number | Required | The id value of the template you want to delete.
##### Example Request
``` shell
curl -u "username:username" -X DELETE 'http://localhost:9000/api/templates/35'
```
##### Example Response
```json
{
"data": true
}
```

View file

@ -0,0 +1,46 @@
# API / Transactional
| Method | Endpoint | Description |
|:-------|:---------|:------------|
| `POST` | /api/tx | |
## POST /api/tx
Send a transactional message to a subscriber using a predefined transactional template.
##### Parameters
| Name | Data Type | Optional | Description |
|:-------------------|:----------|:---------|:----------------------------------------------------------------------------------|
| `subscriber_email` | String | Optional | E-mail of the subscriber. Either this or `subscriber_id` should be passed. |
| `subscriber_id` | Number | Optional | ID of the subscriber. Either this or `subscriber_email` should be passed. |
| `template_id` | Number | Required | ID of the transactional template to use in the message. |
| `from_email` | String | Optional | Optional `from` email. eg: `Company <email@company.com>` |
| `data` | Map | Optional | Optional data in `{}` nested map. Available in the template as `{{ .Tx.Data.* }}` |
| `headers` | []Map | Optional | Optional array of mail headers. `[{"key": "value"}, {"key": "value"}]` |
| `messenger` | String | Optional | Messenger to use to send the message. Default value is `email`. |
| `content_type` | String | Optional | `html`, `markdown`, `plain` |
##### Request
```shell
curl -u "username:password" "http://localhost:9000/api/tx" -X POST \
-H 'Content-Type: application/json; charset=utf-8' \
--data-binary @- << EOF
{
"subscriber_email": "user@test.com",
"template_id": 2,
"data": {"order_id": "1234", "date": "2022-07-30", "items": [1, 2, 3]},
"content_type": "html"
}
EOF
```
##### Response
``` json
{
"data": true
}
```

View file

@ -0,0 +1,48 @@
# Bounce processing
Enable bounce processing in Settings -> Bounces. POP3 bounce scanning and APIs only become available once the setting is enabled.
### POP3 bounce mailbox
Configure the bounce mailbox in Settings -> Bounces. Either the "From" e-mail that is set on a campaign (or in settings) should have a POP3 mailbox behind it to receive bounce e-mails, or you should configure a dedicated POP3 mailbox and add that address as the `Return-Path` (envelope sender) header in Settings -> SMTP -> Custom headers box. For example:
```
[
{"Return-Path": "your-bounce-inbox@site.com"}
]
```
Some mail servers may also return the bounce to the `Reply-To` address, which can also be added to the header settings.
### Webhook API
The bounce webhook API can be used to record bounce events with custom scripting. This could be by reading a mailbox, a database, or mail server logs.
| Method | Endpoint | Description |
|--------|------------------|------------------------|
| `POST` | /webhooks/bounce | Record a bounce event. |
| Name | Data type | Required/Optional | Description |
|-------------------|-----------|-------------------|--------------------------------------------------------------------------------------|
| `subscriber_uuid` | String | Optional | The UUID of the subscriber. Either this or `email` is required. |
| `email` | String | Optional | The e-mail of the subscriber. Either this or `subscriber_uuid` is required. |
| `campaign_uuid` | String | Optional | UUID of the campaign for which the bounce happened. |
| `source` | String | Required | A string indicating the source, eg: `api`, `my_script` etc. |
| `type` | String | Required | `hard` or `soft` bounce. Currently, this has no effect on how the bounce is treated. |
| `meta` | String | Optional | An optional escaped JSON string with arbitrary metadata about the bounce event. |
```shell
curl -u 'username:password' -X POST localhost:9000/webhooks/bounce \
-H "Content-Type: application/json" \
--data '{"email": "user1@mail.com", "campaign_uuid": "9f86b50d-5711-41c8-ab03-bc91c43d711b", "source": "api", "type": "hard", "meta": "{\"additional\": \"info\"}}'
```
### External webhooks
listmonk supports receiving bounce webhook events from the following SMTP providers.
| Endpoint | Description | |
|-----------------------------|------------------|-----------|
| `https://listmonk.yoursite.com/webhooks/service/ses` | Amazon (AWS) SES | You can use these [Mautic steps](https://docs.mautic.org/en/channels/emails/bounce-management#amazon-webhook) as a general guide, but use your listmonk's endpoint instead. <br> * When creating the *topic* select "standard" instead of the preselected "FIFO". You can put a name and leave everything else at default. <br>* When creating a *subscription* choose HTTPS for "Protocol", and leave *"Enable raw message delivery"* UNCHECKED. <br> * On the _"SES -> verified identities"_ page, make sure to check **"[include original headers](https://github.com/knadh/listmonk/issues/720#issuecomment-1046877192)"**. <br> * The Mautic screenshot suggests you should turn off _email feedback forwarding_, but that's completely optional depending on whether you want want email notifications. |
| `https://listmonk.yoursite.com/webhooks/service/sendgrid` | Sendgrid / Twilio Signed event webhook | [More info](https://docs.sendgrid.com/for-developers/tracking-events/getting-started-event-webhook-security-features) |

View file

@ -0,0 +1,72 @@
# Concepts
## Subscriber
A subscriber is a recipient identified by an e-mail address and name. Subscribers receive e-mails that are sent from listmonk. A subscriber can be added to any number of lists. Subscribers who are not a part of any lists are considered *orphan* records.
### Attributes
Attributes are arbitrary properties attached to a subscriber in addition to their e-mail and name. They are represented as a JSON map. It is not necessary for all subscribers to have the same attributes. Subscribers can be [queried and segmented](../querying-and-segmentation) into lists based on their attributes, and the attributes can be inserted into the e-mails sent to them. For example:
```json
{
"city": "Bengaluru",
"likes_tea": true,
"spoken_languages": ["English", "Malayalam"],
"projects": 3,
"stack": {
"frameworks": ["echo", "go"],
"languages": ["go", "python"],
"preferred_language": "go"
}
}
```
### Subscription statuses
A subscriber can be added to one or more lists, and each such relationship can have one of these statuses.
| Status | Description |
| ------------- | --------------------------------------------------------------------------------- |
| `unconfirmed` | The subscriber was added to the list directly without their explicit confirmation. Nonetheless, the subscriber will receive campaign messages sent to single optin campaigns. |
| `confirmed` | The subscriber confirmed their subscription by clicking on 'accept' in the confirmation e-mail. Only confirmed subscribers in opt-in lists will receive campaign messages send to the list. |
| `unsubscribed` | The subscriber is unsubscribed from the list and will not receive any campaign messages sent to the list.
### Segmentation
Segmentation is the process of filtering a large list of subscribers into a smaller group based on arbitrary conditions, primarily based on their attributes. For instance, if an e-mail needs to be sent subscribers who live in a particular city, given their city is described in their attributes, it's possible to quickly filter them out into a new list and e-mail them. [Learn more](../querying-and-segmentation).
## List
A list (or a _mailing list_) is a collection of subscribers grouped under a name, for instance, _clients_. Lists are used to organise subscribers and send e-mails to specific groups. A list can be single optin or double optin. Subscribers added to double optin lists have to explicitly accept the subscription by clicking on the confirmation e-mail they receive. Until then, they do not receive campaign messages.
## Campaign
A campaign is an e-mail (or any other kind of messages) that is sent to one or more lists.
## Transactional message
A transactional message is an arbitrary message sent to a subscriber using the transactional message API. For example a welcome e-mail on signing up to a service; an order confirmation e-mail on purchasing an item; a password reset e-mail when a user initiates an online account recovery process.
## Template
A template is a re-usable HTML design that can be used across campaigns and when sending arbitrary transactional messages. Most commonly, templates have standard header and footer areas with logos and branding elements, where campaign content is inserted in the middle. listmonk supports [Go template](https://gowebexamples.com/templates/) expressions that lets you create powerful, dynamic HTML templates. [Learn more](../templating).
## Messenger
listmonk supports multiple custom messaging backends in additional to the default SMTP e-mail backend, enabling not just e-mail campaigns, but arbitrary message campaigns such as SMS, FCM notifications etc. A *Messenger* is a web service that accepts a campaign message pushed to it as a JSON request, which the service can in turn broadcast as SMS, FCM etc. [Learn more](../messengers).
## Tracking pixel
The tracking pixel is a tiny, invisible image that is inserted into an e-mail body to track e-mail views. This allows measuring the read rate of e-mails. While this is exceedingly common in e-mail campaigns, it carries privacy implications and should be used in compliance with rules and regulations such as GDPR. It is possible to track reads anonymously without associating an e-mail read to a subscriber.
## Click tracking
It is possible to track the clicks on every link that is sent in an e-mail. This allows measuring the clickthrough rates of links in e-mails. While this is exceedingly common in e-mail campaigns, it carries privacy implications and should be used in compliance with rules and regulations such as GDPR. It is possible to track link clicks anonymously without associating an e-mail read to a subscriber.
## Bounce
A bounce occurs when an e-mail that is sent to a recipient "bounces" back for one of many reasons including the recipient address being invalid, their mailbox being full, or the recipient's e-mail service provider marking the e-mail as spam. listmonk can automatically process such bounce e-mails that land in a configured POP mailbox, or via APIs of SMTP e-mail providers such as AWS SES and Sengrid. Based on settings, subscribers returning bounced e-mails can either be blocklisted or deleted automatically. [Learn more](../bounces).

View file

@ -0,0 +1,84 @@
# Configuration
### TOML Configuration file
One or more TOML files can be read by passing `--config config.toml` multiple times. Apart from a few low level configuration variables and the database configuration, all other settings can be managed from the `Settings` dashboard on the admin UI.
To generate a new sample configuration file, run `--listmonk --new-config`
### Environment variables
Variables in config.toml can also be provided as environment variables prefixed by `LISTMONK_` with periods replaced by `__` (double underscore). Example:
| **Environment variable** | Example value |
|--------------------------------|----------------|
| `LISTMONK_app__address` | "0.0.0.0:9000" |
| `LISTMONK_app__admin_username` | listmonk |
| `LISTMONK_app__admin_password` | listmonk |
| `LISTMONK_db__host` | db |
| `LISTMONK_db__port` | 9432 |
| `LISTMONK_db__user` | listmonk |
| `LISTMONK_db__password` | listmonk |
| `LISTMONK_db__database` | listmonk |
| `LISTMONK_db__ssl_mode` | disable |
### Customizing system templates
[Read this](../templating/#system-templates)
### HTTP routes
When configuring auth proxies and web application firewalls, use this table.
#### Private admin endpoints.
| Methods | Route | Description |
|---------|--------------------|-------------------------|
| `*` | `/api/*` | Admin APIs |
| `GET` | `/admin/*` | Admin UI and HTML pages |
| `POST` | `/webhooks/bounce` | Admin bounce webhook |
#### Public endpoints to expose to the internet.
| Methods | Route | Description |
|-------------|-----------------------|-----------------------------------------------|
| `GET, POST` | `/subscription/*` | HTML subscription pages |
| `GET, ` | `/link/*` | Tracked link redirection |
| `GET` | `/campaign/*` | Pixel tracking image |
| `GET` | `/public/*` | Static files for HTML subscription pages |
| `POST` | `/webhooks/service/*` | Bounce webhook endpoints for AWS and Sendgrid |
## Media Uploads
### Filesystem
When configuring `docker` volume mounts for using filesystem media uploads, you can follow either of two approaches.
#### Using volumes
Using `docker volumes`, you can specify the name of volume and destination for the files to be uploaded inside the container.
```yml
app:
volumes:
- type: volume
source: listmonk-uploads
target: /listmonk/uploads
volumes:
listmonk-uploads:
```
!!! note
This volume is managed by `docker` itself, and you can see find the host path with `docker volume inspect listmonk_listmonk-uploads`.
#### Using bind mounts
```yml
volumes:
- /data/uploads:/listmonk/uploads
```
The files will be available inside `/data/uploads` directory on the host machine.

View file

@ -0,0 +1,27 @@
# Developer setup
The app has two distinct components, the Go backend and the VueJS frontend. In the dev environment, both are run independently.
### Pre-requisites
- `go`
- `nodejs` (if you are working on the frontend) and `yarn`
- Postgres database. If there is no local installation, the demo docker DB can be used for development (`docker-compose up demo-db`)
### First time setup
`git clone https://github.com/knadh/listmonk.git`. The project uses go.mod, so it's best to clone it outside the Go src path.
1. Copy `config.toml.sample` as `config.toml` and add your config.
2. `make dist` to build the listmonk binary. Once the binary is built, run `./listmonk --install` to run the DB setup. For subsequent dev runs, use `make run`.
> [mailhog](https://github.com/mailhog/MailHog) is an excellent standalone mock SMTP server (with a UI) for testing and dev.
### Running the dev environment
1. Run `make run` to start the listmonk dev server on `:9000`.
2. Run `make run-frontend` to start the Vue frontend in dev mode using yarn on `:8080`. All `/api/*` calls are proxied to the app running on `:9000`. Refer to the [frontend README](https://github.com/knadh/listmonk/blob/master/frontend/README.md) for an overview on how the frontend is structured.
3. Visit `http://localhost:8080`
# Production build
Run `make dist` to build the Go binary, build the Javascript frontend, and embed the static assets producing a single self-contained binary, `listmonk`

View file

@ -0,0 +1,11 @@
# Integrating with external systems
In many environments, a mailing list manager's subscriber database is not run independently but as a part of an existing customer database or a CRM. There are multiple ways of keeping listmonk in sync with external systems.
## Using APIs
The [subscriber APIs](../apis/subscribers) offers several APIs to manipulate the subscribers database, like addition, updation, and deletion. For bulk synchronisation, a CSV can be generated (and optionally zipped) and posted to the import API.
## Interacting directly with the DB
listmonk uses tables with simple schemas to represent subscribers (`subscribers`), lists (`lists`), and subscriptions (`subscriber_lists`). It is easy to add, update, and delete subscriber information directly with the database tables for advanced usecases. See the [table schemas](https://github.com/knadh/listmonk/blob/master/schema.sql) for more information.

25
docs/docs/content/i18n.md Normal file
View file

@ -0,0 +1,25 @@
# Internationalization (i18n)
listmonk comes available in multiple languages thanks to language packs contributed by volunteers. A language pack is a JSON file with a map of keys and corresponding translations. The bundled languages can be [viewed here](https://github.com/knadh/listmonk/tree/master/i18n).
## Additional language packs
These additional language packs can be downloaded and passed to listmonk with the `--i18n-dir` flag as described in the next section.
| Language | Description |
|------------------|--------------------------------------|
| [Deutsch (formal)](https://raw.githubusercontent.com/SvenPe/listmonk/4bbb2e5ebb2314b754cb2318f4f6683a0f854d43/i18n/de.json) | German language with formal pronouns |
## Customizing languages
To customize an existing language or to load a new language, put one or more `.json` language files in a directory, and pass the directory path to listmonk with the<br />`--i18n-dir=/path/to/dir` flag.
## Contributing a new language
- Visit [https://listmonk.app/i18n](https://listmonk.app/i18n)
- To make changes to an existing language, use the "Load language" option on the top right.
- To create a new language, use the "Load language" option on the top right and select "Default".
- Translate the text in the input fields on the UI.
- Once done, use the `Switch to raw JSON` and copy the JSON data and save it to a file named `xx.json`, where `xx` is the two letter code of the language.
- Send a pull request to add the file to the [i18n directory on the GitHub repo](https://github.com/knadh/listmonk/tree/master/i18n). If you are not familiar with pull requests, share the file by creating a new "issue" (comment) on the [GitHub issues page](https://github.com/knadh/listmonk/issues)

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="163.03" height="30.38" viewBox="0 0 43.135 8.038" xmlns:v="https://vecta.io/nano"><circle cx="4.019" cy="4.019" r="3.149" fill="none" stroke="#0055d4" stroke-width="1.74"/><path d="M11.457 7.303q-.566 0-.879-.322-.313-.331-.313-.932V.712L11.5.572v5.442q0 .305.253.305.139 0 .244-.052l.253.879q-.357.157-.792.157zm2.619-4.754v4.615H12.84V2.549zM13.449.172q.331 0 .54.209.218.2.218.514 0 .313-.218.522-.209.2-.54.2-.331 0-.54-.2-.209-.209-.209-.522 0-.313.209-.514.209-.209.54-.209zm3.319 2.238q.975 0 1.672.557l-.47.705q-.583-.366-1.149-.366-.305 0-.47.113-.165.113-.165.305 0 .139.07.235.078.096.279.183.209.087.618.209.731.2 1.088.54.357.331.357.914 0 .462-.27.801-.261.34-.714.522-.453.174-1.01.174-.583 0-1.062-.174-.479-.183-.819-.496l.61-.679q.583.453 1.237.453.348 0 .549-.131.209-.139.209-.374 0-.183-.078-.287-.078-.104-.287-.192-.209-.096-.653-.218-.697-.192-1.036-.54-.331-.357-.331-.879 0-.392.226-.705.226-.313.636-.488.418-.183.967-.183zm5.342 4.536q-.253.174-.575.261-.313.096-.627.096-.714-.009-1.08-.409-.366-.401-.366-1.176V3.42h-.688v-.871h.688v-1.01l1.237-.148v1.158h1.062l-.122.871h-.94v2.273q0 .331.113.479.113.148.348.148.235 0 .522-.157zm5.493-4.536q.549 0 .879.374.34.374.34 1.019v3.361h-1.237V4.012q0-.679-.453-.679-.244 0-.427.157-.183.157-.374.488v3.187h-1.237V4.012q0-.679-.453-.679-.244 0-.427.165-.183.157-.366.479v3.187h-1.237V2.549h1.071l.096.575q.261-.348.583-.531.331-.183.758-.183.392 0 .679.2.287.192.418.549.287-.374.618-.557.34-.192.766-.192zm4.148 0q1.036 0 1.62.653.583.644.583 1.794 0 .731-.27 1.289-.261.549-.766.853-.496.305-1.176.305-1.036 0-1.628-.644-.583-.653-.583-1.803 0-.731.261-1.28.27-.557.766-.862.505-.305 1.193-.305zm0 .923q-.47 0-.705.374-.226.366-.226 1.149 0 .784.226 1.158.235.366.697.366.462 0 .688-.366.235-.374.235-1.158 0-.784-.226-1.149-.226-.374-.688-.374zm5.271-.923q.61 0 .949.374.34.366.34 1.019v3.361h-1.237V4.012q0-.374-.131-.522-.122-.157-.374-.157-.261 0-.479.165-.209.157-.409.479v3.187h-1.237V2.549h1.071l.096.583q.287-.357.627-.54.348-.183.784-.183zM40.2.572v6.592h-1.237V.712zm2.804 1.977l-1.472 2.029 1.602 2.586h-1.402l-1.489-2.525 1.48-2.09z"/></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

View file

@ -0,0 +1,10 @@
# Introduction
[![listmonk](images/logo.svg)](https://listmonk.app)
listmonk is a self-hosted, high performance mailing list and newsletter manager. It comes as a standalone binary and the only dependency is a Postgres database.
[![listmonk screenshot](https://user-images.githubusercontent.com/547147/134939475-e0391111-f762-44cb-b056-6cb0857755e3.png)](https://listmonk.app)
## Developers
listmonk is a free and open source software licensed under AGPLv3. If you are interested in contributing, check out the [GitHub repository](https://github.com/knadh/listmonk) and refer to the [developer setup](developer-setup). The backend is written in Go and the frontend is Vue with Buefy for UI.

View file

@ -0,0 +1,136 @@
# Installation
listmonk requires Postgres ⩾ v9.4.
## Binary
- Download the [latest release](https://github.com/knadh/listmonk/releases) and extract the listmonk binary.
- `./listmonk --new-config` to generate config.toml. Then, edit the file.
- `./listmonk --install` to install the tables in the Postgres DB.
- Run `./listmonk` and visit `http://localhost:9000`.
## Docker
The latest image is available on DockerHub at `listmonk/listmonk:latest`
Use the sample [docker-compose.yml](https://github.com/knadh/listmonk/blob/master/docker-compose.yml) to run listmonk and Postgres DB with docker-compose as follows:
### Demo
#### Easy Docker install
```bash
mkdir listmonk-demo
sh -c "$(curl -fsSL https://raw.githubusercontent.com/knadh/listmonk/master/install-demo.sh)"
```
#### Manual Docker install
```bash
wget -O docker-compose.yml https://raw.githubusercontent.com/knadh/listmonk/master/docker-compose.yml
docker-compose up -d demo-db demo-app
```
!!! warning
The demo does not persist Postgres after the containers are removed. **DO NOT** use this demo setup in production.
### Production
#### Easy Docker install
This setup is recommended if you want to _quickly_ setup `listmonk` in production.
```bash
mkdir listmonk
sh -c "$(curl -fsSL https://raw.githubusercontent.com/knadh/listmonk/master/install-prod.sh)"
```
The above shell script performs the following actions:
- Downloads `docker-compose.yml` and generates a `config.toml`.
- Runs a Postgres container and installs the database schema.
- Runs the `listmonk` container.
!!! note
It's recommended to examine the contents of the shell script, before running in your environment.
#### Manual Docker install
The following workflow is recommended to setup `listmonk` manually using `docker-compose`. You are encouraged to customise the contents of `docker-compose.yml` to your needs. The overall setup looks like:
- `docker-compose up db` to run the Postgres DB.
- `docker-compose run --rm app ./listmonk --install` to setup the DB (or `--upgrade` to upgrade an existing DB).
- Copy `config.toml.sample` to your directory and make the following changes:
- `app.address` => `0.0.0.0:9000` (Port forwarding on Docker will work only if the app is advertising on all interfaces.)
- `db.host` => `listmonk_db` (Container Name of the DB container)
- Run `docker-compose up app` and visit `http://localhost:9000`.
##### Mounting a custom config.toml
To mount a local `config.toml` file, add the following section to `docker-compose.yml`:
```yml
app:
<<: *app-defaults
depends_on:
- db
volumes:
- ./path/on/your/host/config.toml:/listmonk/config.toml
```
!!! note
Some common changes done inside `config.toml` for Docker based setups:
- Change `app.address` to `0.0.0.0:9000`.
- Change `db.host` to `listmonk_db`.
Here's a sample `config.toml` you can use:
```toml
[app]
address = "0.0.0.0:9000"
admin_username = "listmonk"
admin_password = "listmonk"
# Database.
[db]
host = "listmonk_db"
port = 5432
user = "listmonk"
password = "listmonk"
database = "listmonk"
ssl_mode = "disable"
max_open = 25
max_idle = 25
max_lifetime = "300s"
```
Mount the local `config.toml` inside the container at `listmonk/config.toml`.
!!! tip
- See [configuring with environment variables](../configuration) for variables like `app.admin_password` and `db.password`
- Ensure that both `app` and `db` containers are in running. If the containers are not running, restart them `docker-compose restart app db`.
- Refer to [this tutorial](https://yasoob.me/posts/setting-up-listmonk-opensource-newsletter-mailing/) for setting up a production instance with Docker + Nginx + LetsEncrypt SSL.
!!! info
The example `docker-compose.yml` file works with Docker Engine 18.06.0+ and `docker-compose` which supports file format 3.7.
## Compiling from source
To compile the latest unreleased version (`master` branch):
1. Make sure `go`, `nodejs`, and `yarn` are installed on your system.
2. `git clone git@github.com:knadh/listmonk.git`
3. `cd listmonk && make dist`. This will generate the `listmonk binary`.
## Release candidate (RC)
The `master` branch with bleeding edge changes is periodically built and published as `listmonk/listmonk:rc` on DockerHub. To run the latest pre-release version, replace all instances of `listmonk/listmonk:latest` with `listmonk/listmonk:rc` in the docker-compose.yml file and follow the Docker installation steps above. While it is generally safe to run release candidate versions, they may have issues that only get resolved in a general release.
## 3rd party hosting
<a href="https://railway.app/new/template/listmonk"><img src="https://camo.githubusercontent.com/081df3dd8cff37aab35044727b02b94a8e948052487a8c6253e190f5940d776d/68747470733a2f2f7261696c7761792e6170702f627574746f6e2e737667" alt="One-click deploy on Raleway" style="max-height: 32px;" /></a>
<br />
<a href="https://www.pikapods.com/pods?run=listmonk"><img src="https://www.pikapods.com/static/run-button.svg" alt="Deploy on PikaPod" /></a>
<a href ="https://github.com/paulrudy/listmonk-on-fly">Tutorial for deploying on Fly.io</a>

View file

@ -0,0 +1,43 @@
# Messengers
listmonk supports multiple custom messaging backends in additional to the default SMTP e-mail backend, enabling not just e-mail campaigns, but arbitrary message campaigns such as SMS, FCM notifications etc.
A *Messenger* is a web service that accepts a campaign message pushed to it as a JSON request, which the service can in turn broadcast as SMS, FCM etc. Messengers are registered in the *Settings -> Messengers* UI, and can be selected on individual campaigns.
Messengers support optional BasicAuth authentication. `Plain text` format for campaign content is ideal for messengers such as SMS and FCM.
When a campaign starts, listmonk POSTs messages in the following format to the selected messenger's endpoint. The endpoint should return a `200 OK` response in case of a successful request.
The address required to broadcast the message, for instance, a phone number or an FCM ID, is expected to be stored and relayed as [subscriber attributes](../concepts/#attributes).
```json
{
"subject": "Welcome to listmonk",
"body": "The message body",
"content_type": "plain",
"recipients": [{
"uuid": "e44b4135-1e1d-40c5-8a30-0f9a886c2884",
"email": "anon@example.com",
"name": "Anon Doe",
"attribs": {
"phone": "123123123",
"fcm_id": "2e7e4b512e7e4b512e7e4b51",
"city": "Bengaluru"
},
"status": "enabled"
}],
"campaign": {
"uuid": "2e7e4b51-f31b-418a-a120-e41800cb689f",
"name": "Test campaign",
"tags": ["test-campaign"]
}
}
```
## Messenger implementations
Following is a list of HTTP messenger servers that connect to various backends.
| Name | Backend |
|------------------------------------------------------------------------|------------------|
| [listmonk-messenger](https://github.com/joeirimpan/listmonk-messenger) | AWS Pinpoint SMS |

View file

@ -0,0 +1,95 @@
# Querying and segmenting subscribers
listmonk allows the writing of partial Postgres SQL expressions to query, filter, and segment subscribers.
## Database fields
These are the fields in the subscriber database that can be queried.
| Field | Description |
| ------------------------ | --------------------------------------------------------------------------------------------------- |
| `subscribers.uuid` | The randomly generated unique ID of the subscriber |
| `subscribers.email` | E-mail ID of the subscriber |
| `subscribers.name` | Name of the subscriber |
| `subscribers.status` | Status of the subscriber (enabled, disabled, blocklisted) |
| `subscribers.attribs` | Map of arbitrary attributes represented as JSON. Accessed via the `->` and `->>` Postgres operator. |
| `subscribers.created_at` | Timestamp when the subscriber was first added |
| `subscribers.updated_at` | Timestamp when the subscriber was modified |
## Sample attributes
Here's a sample JSON map of attributes assigned to an imaginary subscriber.
```json
{
"city": "Bengaluru",
"likes_tea": true,
"spoken_languages": ["English", "Malayalam"],
"projects": 3,
"stack": {
"frameworks": ["echo", "go"],
"languages": ["go", "python"],
"preferred_language": "go"
}
}
```
![listmonk screenshot](images/edit-subscriber.png)
## Sample SQL query expressions
![listmonk](images/query-subscribers.png)
#### Find a subscriber by e-mail
```sql
-- Exact match
subscribers.email = 'some@domain.com'
-- Partial match to find e-mails that end in @domain.com.
subscribers.email LIKE '%@domain.com'
```
#### Find a subscriber by name
```sql
-- Find all subscribers whose name start with John.
subscribers.email LIKE 'John%'
```
#### Multiple conditions
```sql
-- Find all Johns who have been blocklisted.
subscribers.email LIKE 'John%' AND status = 'blocklisted'
```
#### Querying attributes
```sql
-- The ->> operator returns the value as text. Find all subscribers
-- who live in Bengaluru and have done more than 3 projects.
-- Here 'projects' is cast into an integer so that we can apply the
-- numerical operator >
subscribers.attribs->>'city' = 'Bengaluru' AND
(subscribers.attribs->>'projects')::INT > 3
```
#### Querying nested attributes
```sql
-- Find all blocklisted subscribers who like to drink tea, can code Python
-- and prefer coding Go.
--
-- The -> operator returns the value as a structure. Here, the "languages" field
-- The ? operator checks for the existence of a value in a list.
subscribers.status = 'blocklisted' AND
(subscribers.attribs->>'likes_tea')::BOOLEAN = true AND
subscribers.attribs->'stack'->'languages' ? 'python' AND
subscribers.attribs->'stack'->>'preferred_language' = 'go'
```
To learn how to write SQL expressions to do advancd querying on JSON attributes, refer to the Postgres [JSONB documentation](https://www.postgresql.org/docs/11/functions-json.html).

View file

@ -0,0 +1,100 @@
body[data-md-color-primary="white"] .md-header[data-md-state="shadow"] {
background: #fff;
box-shadow: none;
color: #333;
box-shadow: 1px 1px 3px #ddd;
}
.md-typeset .md-typeset__table table {
border: 1px solid #ddd;
box-shadow: 2px 2px 0 #f3f3f3;
overflow: inherit;
}
body[data-md-color-primary="white"] .md-search__input {
background: #f6f6f6;
color: #333;
}
body[data-md-color-primary="white"]
.md-sidebar--secondary
.md-sidebar__scrollwrap {
background: #f6f6f6;
padding: 10px 0;
}
body[data-md-color-primary="white"] .md-nav__item--active {
font-weight: 600;
color: inherit;
}
body[data-md-color-primary="white"] .md-nav__item--active a {
color: #0055d4;
}
body[data-md-color-primary="white"] .md-nav__item a:hover {
color: #0055d4;
}
body[data-md-color-primary="white"] thead,
body[data-md-color-primary="white"] .md-typeset table:not([class]) th {
background: #f6f6f6;
border: 0;
color: inherit;
font-weight: 600;
}
table td span {
font-size: 0.85em;
color: #bbb;
display: block;
}
.md-typeset h1, .md-typeset h2 {
font-weight: 500;
}
body[data-md-color-primary="white"] .md-typeset h1 {
margin: 4rem 0 0 0;
color: inherit;
border-top: 1px solid #ddd;
padding-top: 2rem;
}
body[data-md-color-primary="white"] .md-typeset h2 {
border-top: 1px solid #eee;
padding-top: 2rem;
}
body[data-md-color-primary="white"] .md-content h1:first-child {
margin: 0 0 3rem 0;
padding: 0;
border: 0;
}
body[data-md-color-primary="white"] .md-typeset code {
word-break: normal;
}
li img {
background: #fff;
border-radius: 6px;
border: 1px solid #e6e6e6;
box-shadow: 1px 1px 4px #e6e6e6;
padding: 5px;
margin-top: 10px;
}
/* This hack places the #anchor-links correctly
by accommodating for the fixed-header's height */
:target:before {
content: "";
display: block;
height: 120px;
margin-top: -120px;
}
.md-typeset a {
color: #0055d4;
}
.md-typeset a:hover {
color: #666 !important;
text-decoration: underline;
}

View file

@ -0,0 +1,170 @@
# Templating
A template is a re-usable HTML design that can be used across campaigns and transactional messages. Most commonly, templates have standard header and footer areas with logos and branding elements, where campaign content is inserted in the middle. listmonk supports Go template expressions that lets you create powerful, dynamic HTML templates.
listmonk supports [Go template](https://gowebexamples.com/templates/) expressions that lets you create powerful, dynamic HTML templates. It also integrates 100+ useful [Sprig template functions](https://masterminds.github.io/sprig/).
## Campaign templates
Campaign templates are used in an e-mail campaigns. These template are created and managed on the UI under `Campaigns -> Templates`, and are selected when creating new campaigns.
## Transactional templates
Transactional templates are used for sending arbitrary transactional messages using the transactional API. These template are created and managed on the UI under `Campaigns -> Templates`.
## Template expressions
There are several template functions and expressions that can be used in campaign and template bodies. They are written in the form `{{ .Subscriber.Email }}`, that is, an expression between double curly braces `{{` and `}}`.
### Subscriber fields
| Expression | Description |
| ----------------------------- | -------------------------------------------------------------------------------------------- |
| `{{ .Subscriber.UUID }}` | The randomly generated unique ID of the subscriber |
| `{{ .Subscriber.Email }}` | E-mail ID of the subscriber |
| `{{ .Subscriber.Name }}` | Name of the subscriber |
| `{{ .Subscriber.FirstName }}` | First name of the subscriber (automatically extracted from the name) |
| `{{ .Subscriber.LastName }}` | Last name of the subscriber (automatically extracted from the name) |
| `{{ .Subscriber.Status }}` | Status of the subscriber (enabled, disabled, blocklisted) |
| `{{ .Subscriber.Attribs }}` | Map of arbitrary attributes. Fields can be accessed with `.`, eg: `.Subscriber.Attribs.city` |
| `{{ .Subscriber.CreatedAt }}` | Timestamp when the subscriber was first added |
| `{{ .Subscriber.UpdatedAt }}` | Timestamp when the subscriber was modified |
| Expression | Description |
| --------------------- | -------------------------------------------------------- |
| `{{ .Campaign.UUID }}` | The randomly generated unique ID of the campaign |
| `{{ .Campaign.Name }}` | Internal name of the campaign |
| `{{ .Campaign.Subject }}` | E-mail subject of the campaign |
| `{{ .Campaign.FromEmail }}` | The e-mail address from which the campaign is being sent |
### Functions
| Function | Description |
| ------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `{{ Date "2006-01-01" }}` | Prints the current datetime for the given format expressed as a [Go date layout](https://yourbasic.org/golang/format-parse-string-time-date-example/) |
| `{{ TrackLink "https://link.com" }}` | Takes a URL and generates a tracking URL over it. For use in campaign bodies and templates. |
| `https://link.com@TrackLink` | Shorthand for `TrackLink`. Eg: `<a href="https://link.com@TrackLink">Link</a>` |
| `{{ TrackView }}` | Inserts a single tracking pixel. Should only be used once, ideally in the template footer. |
| `{{ UnsubscribeURL }}` | Unsubscription and Manage preferences URL. Ideal for use in the template footer. |
| `{{ MessageURL }}` | URL to view the hosted version of an e-mail message. |
| `{{ OptinURL }}` | URL to the double-optin confirmation page. |
| `{{ Safe "<!-- comment -->" }}` | Add any HTML code as it is. |
### Sprig functions
listmonk integrates the Sprig library that offers 100+ utility functions for working with strings, numbers, dates etc. that can be used in templating. Refer to the [Sprig documentation](https://masterminds.github.io/sprig/) for the full list of functions.
### Example template
The expression `{{ template "content" . }}` should appear exactly once in every template denoting the spot where an e-mail's content is inserted. Here's a sample HTML e-mail that has a fixed header and footer that inserts the content in the middle.
```html
<!DOCTYPE html>
<html>
<head>
<style>
body {
background: #eee;
font-family: Arial, sans-serif;
font-size: 6px;
color: #111;
}
header {
border-bottom: 1px solid #ddd;
padding-bottom: 30px;
margin-bottom: 30px;
}
.container {
background: #fff;
width: 450px;
margin: 0 auto;
padding: 30px;
}
</style>
</head>
<body>
<section class="container">
<header>
<!-- This will appear in the header of all e-mails.
The subscriber's name will be automatically inserted here. //-->
Hi {{ .Subscriber.FirstName }}!
</header>
<!-- This is where the e-mail body will be inserted //-->
<div class="content">
{{ template "content" . }}
</div>
<footer>
Copyright 2019. All rights Reserved.
</footer>
<!-- The tracking pixel will be inserted here //-->
{{ TrackView }}
</section>
</body>
</html>
```
!!! info
For use with plaintext campaigns, create a template with no HTML content and just the placeholder `{{ template "content" . }}`
### Example campaign body
Campaign bodies can be composed using the built-in WYSIWYG editor or as raw HTML documents. Assuming that the subscriber has a set of [attributes defined](../querying-and-segmentation#sample-attributes), this example shows how to render those values in a campaign.
```
Hey, did you notice how the template showed your first name?
Your last name is {{.Subscriber.LastName }}.
You have done {{ .Subscriber.Attribs.projects }} projects.
{{ if eq .Subscriber.Attribs.city "Bengaluru" }}
You live in Bangalore!
{{ else }}
Where do you live?
{{ end }}
Here is a link for you to click that will be tracked.
<a href="{{ TrackLink "https://google.com" }}">Google</a>
```
The above example uses an `if` condition to show one of two messages depending on the value of a subscriber attribute. Many such dynamic expressions are possible with Go templating expressions.
## System templates
System templates are used for rendering public user facing pages such as the subscription management page, and in automatically generated system e-mails such as the opt-in confirmation e-mail. These are bundled into listmonk but can be customized by copying the [static directory](https://github.com/knadh/listmonk/tree/master/static) locally, and passing its path to listmonk with the `./listmonk --static-dir=your/custom/path` flag.
### Public pages
| /static/public/ | |
|------------------------|--------------------------------------------------------------------|
| `index.html` | Base template with the header and footer that all pages use. |
| `home.html` | Landing page on the root domain with the login button. |
| `message.html` | Generic success / failure message page. |
| `optin.html` | Opt-in confirmation page. |
| `subscription.html` | Subscription management page with options for data export and wipe. |
| `subscription-form.html` | List selection and subscription form page. |
To edit the appearance of the public pages using CSS and Javascript, head to Settings > Appearance > Public:
![image](https://user-images.githubusercontent.com/55474996/153739792-93074af6-d1dd-40aa-8cde-c02ea4bbb67b.png)
### System e-mails
| /static/email-templates/ | |
|----------------------------------|------------------------------------------------------------------------------------------------------------------------------------|
| `base.html` | Base template with the header and footer that all system generated e-mails use. |
| `campaign-status.html` | E-mail notification that is sent to admins on campaign start, completion etc. |
| `import-status.html` | E-mail notification that is sent to admins on finish of an import job. |
| `subscriber-data.html` | E-mail that is sent to subscribers when they request a full dump of their private data. |
| `subscriber-optin.html` | Automatic opt-in confirmation e-mail that is sent to an unconfirmed subscriber when they are added. |
| `subscriber-optin-campaign.html` | E-mail content that's inserted into a campaign body when starting an opt-in campaign from the lists page. |
| `default.tpl` | Default campaign template that is created in Campaigns -> Templates when listmonk is first installed. This is not used after that. |
!!! info
To turn system e-mail templates to plaintext, remove `<!doctype html>` from base.html and remove all HTML tags from the templates while retaining the Go templating code.

View file

@ -0,0 +1,20 @@
# Upgrade
Some versions may require changes to the database. These changes or database "migrations" are applied automatically and safely, but, it is recommended to take a backup of the Postgres database before running the `--upgrade` option, especially if you have made customizations to the database tables.
## Binary
- Download the [latest release](https://github.com/knadh/listmonk/releases) and extract the listmonk binary.
- `./listmonk --upgrade` to upgrade an existing DB. Upgrades are idempotent and running them multiple times have no side effects.
- Run `./listmonk` and visit `http://localhost:9000`.
## Docker
- `docker-compose pull` to pull the latest version from DockerHub.
- `docker-compose run --rm app ./listmonk --upgrade` to upgrade an existing DB.
- Run `docker-compose up app db` and visit `http://localhost:9000`.
## Railway
- Head to your dashboard, and select your Listmonk project.
- Select the GitHub deployment service.
- In the Deployment tab, head to the latest deployment, click on the three vertical dots to the right, and select "Redeploy".
- ![Railway Redeploy option](https://user-images.githubusercontent.com/55474996/226517149-6dc512d5-f862-46f7-a57d-5e55b781ff53.png)

58
docs/docs/mkdocs.yml Normal file
View file

@ -0,0 +1,58 @@
site_name: listmonk / Documentation
theme:
name: material
# custom_dir: "mkdocs-material/material"
logo: "images/favicon.png"
favicon: "images/favicon.png"
language: "en"
font:
text: 'Inter'
weights: 400
direction: 'ltr'
extra:
search:
language: 'en'
feature:
tabs: true
palette:
primary: "white"
accent: "red"
site_dir: _out
docs_dir: content
markdown_extensions:
- admonition
- pymdownx.highlight
- pymdownx.superfences
- toc:
permalink: true
extra_css:
- "static/style.css"
copyright: "Copyright &copy; 2019-2023, Kailash Nadh."
nav:
- "Introduction": index.md
- "Installation": installation.md
- "Upgrade": upgrade.md
- "Configuration": configuration.md
- "Developer setup": developer-setup.md
- "Concepts": concepts.md
- "Querying and segmenting subscribers": querying-and-segmentation.md
- "Templating": templating.md
- "Bounce processing": bounces.md
- "Messengers": "messengers.md"
- "Internationalization": "i18n.md"
- "Integrating with external systems": external-integration.md
- "API": apis/apis.md
- "API / Subscribers": apis/subscribers.md
- "API / Lists": apis/lists.md
- "API / Import": apis/import.md
- "API / Campaigns": apis/campaigns.md
- "API / Media": apis/media.md
- "API / Templates": apis/templates.md
- "API / Transactional": apis/transactional.md

96
docs/i18n/index.html Normal file
View file

@ -0,0 +1,96 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>listmonk i18n translation editor</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<link rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap" rel="stylesheet">
<link rel="stylesheet" type="text/css" href="style.css" />
</head>
<body>
<div id="app" class="container">
<header class="header">
<h1 class="title">{{ values["_.name"] }}</h1>
<div class="controls">
<div class="import block">
<a v-if="!isRawVisible" href="#" @click.prevent="onToggleRaw">Switch to raw JSON</a>
<a v-else href="#" @click.prevent="onToggleRaw">Switch to editor</a>
</div>
<div class="view block">
<label for="view-all" class="all">
<input v-model="view" name="view" id="view-all" type="radio" value="all" checked="true" />
All ({{ keys.length }})
</label>
<label for="view-pending" class="pending">
<input v-model="view" name="view" id="view-pending" type="radio" value="pending" />
Pending ({{ keys.length - completed }})
</label>
<label for="view-complete" class="complete">
<input v-model="view" name="view" id="view-complete" type="radio" value="complete" />
Complete ({{ completed }})
</label>
</div>
<div class="selector block">
Load language
<select v-model="loadLang" @change="onLoadLanguage">
<option value="en">Default (en)</option>
<option value="ca"> Català (ca) </option>
<option value="cs-cz"> čeština (cs) </option>
<option value="de"> Deutsch (de) </option>
<option value="es"> Español (es) </option>
<option value="fi"> Suomi (fi) </option>
<option value="fr"> Français (fr) </option>
<option value="hu"> Hungary (hu) </option>
<option value="it"> Italiano (it) </option>
<option value="jp"> 日本語 (jp) </option>
<option value="ml"> മലയാളം (ml) </option>
<option value="nl"> Nederlands (nl) </option>
<option value="pl"> Polski (pl) </option>
<option value="pt"> Portuguese (pt) </option>
<option value="pt-BR"> Português Brasileiro (pt-BR) </option>
<option value="ro"> Română (ro) </option>
<option value="ru"> Русский (ru) </option>
<option value="tr"> Turkish (tr) </option>
<option value="vi"> Vietnamese (vi) </option>
<option value="zh-CN"> 简体中文 (zh-CN) </option>
<option value="zh-TW"> 繁體中文(zh-TW) </option>
</select>
</div>
</div>
</header>
<p>
Changes are stored in the browser's localStorage until the cache is cleared.
To edit an existing language, load it and edit the fields.
To create a new language, load the default language and edit the fields.
Once done, copy the raw JSON and send a PR to the
<a href="https://github.com/knadh/listmonk/tree/i18n/i18n" target="_blank">repo</a>.
</p>
<div v-if="!isRawVisible" class="data">
<div :class="{'item': true, 'done': isDone(k.key)}" v-for="(k, i) in keys" v-if="isItemVisible(k.key)">
<h3 class="head" v-if="k.head">{{ k.head }}</h3>
<div class="controls">
<div class="num">{{ i + 1 }}.</div>
<div class="fields">
<span class="base">{{ base[k.key] }}</span>
<input type="text" v-model="values[k.key]" @blur="saveData" />
<label class="key">{{ k.key }}</label>
</div>
</div>
</div>
</div><!-- data -->
<div v-else class="raw">
<textarea v-model="rawData"></textarea>
</div><!-- raw -->
</div>
<h4 id="loading">Loading ...</h4>
<script src="vue.min.js"></script>
<script src="main.js"></script>
</body>
</html>

156
docs/i18n/main.js Normal file
View file

@ -0,0 +1,156 @@
const BASEURL = "https://raw.githubusercontent.com/knadh/listmonk/master/i18n/";
const BASELANG = "en";
var app = new Vue({
el: "#app",
data: {
base: {},
keys: [],
visibleKeys: {},
values: {},
view: "all",
loadLang: BASELANG,
isRawVisible: false,
rawData: "{}"
},
methods: {
init() {
document.querySelector("#app").style.display = 'block';
document.querySelector("#loading").remove();
},
loadBaseLang(url) {
return fetch(url).then(response => response.json()).then(data => {
// Retain the base values.
Object.assign(this.base, data);
// Get the sorted keys from the language map.
const keys = [];
const visibleKeys = {};
let head = null;
Object.entries(this.base).sort((a, b) => a[0].localeCompare(b[0])).forEach((v) => {
const h = v[0].split('.')[0];
keys.push({
"key": v[0],
"head": (head !== h ? h : null) // eg: campaigns on `campaigns.something.else`
});
visibleKeys[v[0]] = true;
head = h;
});
this.keys = keys;
this.visibleKeys = visibleKeys;
this.values = { ...this.base };
// Is there cached localStorage data?
if(localStorage.data) {
try {
this.loadData(JSON.parse(localStorage.data));
} catch(e) {
console.log("Bad JSON in localStorage: " + e.toString());
}
return;
}
});
},
loadData(data) {
// Filter out all keys from data except for the base ones
// in the base language.
const vals = this.keys.reduce((a, key) => {
a[key.key] = data.hasOwnProperty(key.key) ? data[key.key] : this.base[key.key];
return a;
}, {});
this.values = vals;
this.saveData();
},
saveData() {
localStorage.data = JSON.stringify(this.values);
},
// Has a key been translated (changed from the base)?
isDone(key) {
return this.values[key] && this.base[key] !== this.values[key];
},
isItemVisible(key) {
return this.visibleKeys[key];
},
onToggleRaw() {
if (!this.isRawVisible) {
this.rawData = JSON.stringify(this.values, Object.keys(this.values).sort(), 4);
} else {
try {
this.loadData(JSON.parse(this.rawData));
} catch (e) {
alert("error parsing JSON: " + e.toString());
return false;
}
}
this.isRawVisible = !this.isRawVisible;
},
onLoadLanguage() {
if(!confirm("Loading this language will overwrite your local changes. Continue?")) {
return false;
}
fetch(BASEURL + this.loadLang + ".json").then(response => response.json()).then(data => {
this.loadData(data);
}).catch((e) => {
console.log(e);
alert("error fetching file: " + e.toString());
});
}
},
mounted() {
this.loadBaseLang(BASEURL + BASELANG + ".json").then(() => this.init());
},
watch: {
view(v) {
// When the view changes, create a copy of the items to be filtered
// by and filter the view based on that. Otherwise, the moment the value
// in the input changes, the list re-renders making items disappear.
const visibleKeys = {};
this.keys.forEach((k) => {
let visible = true;
if (v === "pending") {
visible = !this.isDone(k.key);
} else if (v === "complete") {
visible = this.isDone(k.key);
}
if(visible) {
visibleKeys[k.key] = true;
}
});
this.visibleKeys = visibleKeys;
}
},
computed: {
completed() {
let n = 0;
this.keys.forEach(k => {
if(this.values[k.key] !== this.base[k.key]) {
n++;
}
});
return n;
}
}
});

106
docs/i18n/style.css Normal file
View file

@ -0,0 +1,106 @@
* {
box-sizing: border-box;
}
body {
font-family: Inter, "Helvetica Neue", "Segoe UI", sans-serif;
font-size: 16px;
line-height: 24px;
}
h1, h2, h3, h4, h5 {
margin: 0 0 15px 0;
}
.container {
padding: 30px;
}
.header {
align-items: center;
margin-bottom: 30px;
}
.header .controls {
display: flex;
}
.header .controls .pending {
color: #ff3300;
}
.header .controls .complete {
color: #05a200;
}
.header .title {
margin: 0 0 15px 0;
}
.header .block {
margin: 0 45px 0 0;
}
.header .view label {
cursor: pointer;
margin-right: 10px;
display: inline-block;
}
#app {
display: none;
}
.data .key,
.data .base {
display: block;
color: #777;
display: block;
}
.data .item {
padding: 15px;
clear: both;
}
.data .item:hover {
background: #eee;
}
.data .item.done .num {
color: #05a200;
}
.data .item.done .num::after {
content: '✓';
font-weight: bold;
}
.data .controls {
display: flex;
}
.data .fields {
flex-grow: 1;
}
.data .num {
margin-right: 15px;
min-width: 50px;
}
.data .key {
color: #aaa;
font-size: 0.875em;
}
.data input {
width: 100%;
border: 1px solid #ddd;
padding: 5px;
display: block;
margin: 3px 0;
}
.data input:focus {
border-color: #666;
}
.data p {
margin: 0 0 3px 0;
}
.data .head {
margin: 0 0 15px 0;
}
.raw textarea {
border: 1px solid #ddd;
padding: 5px;
width: 100%;
height: 90vh;
}

6
docs/i18n/vue.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

6
docs/site/config.toml Normal file
View file

@ -0,0 +1,6 @@
baseurl = "https://listmonk.app/"
languageCode = "en-us"
title = "listmonk - Free and open source self-hosted newsletter, mailing list manager, and transactional mails"
[taxonomies]
tag = "tags"

1
docs/site/content/.gitignore vendored Normal file
View file

@ -0,0 +1 @@

View file

@ -0,0 +1 @@
{"version":"v2.4.0","date":"2023-03-20T13:54:12Z","url":"https://github.com/knadh/listmonk/releases/tag/v2.4.0","assets":[{"name":"darwin","url":"https://github.com/knadh/listmonk/releases/download/v2.4.0/listmonk_2.4.0_darwin_amd64.tar.gz"},{"name":"freebsd","url":"https://github.com/knadh/listmonk/releases/download/v2.4.0/listmonk_2.4.0_freebsd_amd64.tar.gz"},{"name":"linux","url":"https://github.com/knadh/listmonk/releases/download/v2.4.0/listmonk_2.4.0_linux_amd64.tar.gz"},{"name":"netbsd","url":"https://github.com/knadh/listmonk/releases/download/v2.4.0/listmonk_2.4.0_netbsd_amd64.tar.gz"},{"name":"openbsd","url":"https://github.com/knadh/listmonk/releases/download/v2.4.0/listmonk_2.4.0_openbsd_amd64.tar.gz"},{"name":"windows","url":"https://github.com/knadh/listmonk/releases/download/v2.4.0/listmonk_2.4.0_windows_amd64.tar.gz"}]}

View file

@ -0,0 +1,219 @@
{{ partial "header.html" . }}
<div class="splash container center">
<img class="s4" src="static/images/s4.png" />
<div class="hero">
<h1 class="title">Self-hosted newsletter and mailing list manager</h1>
<h3 class="sub">
Performance and features packed into a single binary.<br />
<strong>Free and open source.</strong>
</h3>
<p class="center demo">
<a href="https://demo.listmonk.app" class="button">Live demo</a>
</p>
</div>
<div class="confetti">
<img class="s1" src="static/images/s1.png" />
<img class="s2" src="static/images/s2.png" />
<img class="s3" src="static/images/s3.png" />
<img class="box" src="{{ .Site.BaseURL }}static/images/splash.png" alt="listmonk screenshot" />
</div>
</div>
</div>
<section id="download">
<div class="container">
<h2 class="center">Download</h2>
<p class="center">
The latest version is <strong>{{ .Page.Site.Data.github.version }}</strong>
released on {{ .Page.Site.Data.github.date | dateFormat "02 Jan 2006" }}.
See <a href="{{ .Page.Site.Data.github.url }}">release notes.</a>
</p><br />
<div class="row">
<div class="col-6">
<div class="box">
<h3>Binary</h3>
<ul class="install-steps">
<li class="download-links">Download binary:<br />
{{ range.Page.Site.Data.github.assets }}
<a href="{{ .url }}">{{ .name | title }}</a>
{{ end }}
</li>
<li>
<code>./listmonk --new-config</code> to generate config.toml. Edit the file.
</li>
<li><code>./listmonk --install</code> to setup the Postgres DB (⩾ v9.4) or <code>--upgrade</code> to upgrade an existing DB.</li>
<li>Run <code>./listmonk</code> and visit <code>http://localhost:9000</code></li>
</ul>
<p><a href="/docs/installation">Installation docs &rarr;</a></p>
<br />
<h3>Hosting providers</h3>
<a href="https://railway.app/new/template/listmonk"><img src="https://camo.githubusercontent.com/081df3dd8cff37aab35044727b02b94a8e948052487a8c6253e190f5940d776d/68747470733a2f2f7261696c7761792e6170702f627574746f6e2e737667" alt="One-click deploy on Raleway" style="max-height: 32px;" /></a>
<br />
<a href="https://www.pikapods.com/pods?run=listmonk"><img src="https://www.pikapods.com/static/run-button.svg" alt="Deploy on PikaPod" /></a>
<br />
<a href="https://dash.elest.io/deploy?soft=Listmonk&id=237"><img height="33" src="https://github.com/elestio-examples/wordpress/raw/main/deploy-on-elestio.png" alt="Deploy on Elestio" /></a>
</div>
</div>
<div class="col-6">
<div class="box">
<h3>Docker</h3>
<p><a href="https://hub.docker.com/r/listmonk/listmonk/tags?page=1&ordering=last_updated&name=latest"><code>listmonk/listmonk:latest</code></a></p>
<p>
Use the sample <a href="https://github.com/knadh/listmonk/blob/master/docker-compose.yml">docker-compose.yml</a>
to run manually or use the helper script.
</p>
<h4>Demo</h4>
<pre>mkdir listmonk-demo && cd listmonk-demo
sh -c "$(curl -fsSL https://raw.githubusercontent.com/knadh/listmonk/master/install-demo.sh)"</pre>
<p>
(DO NOT use this demo setup in production)
</p>
<h4>Production</h4>
<pre>mkdir listmonk && cd listmonk
sh -c "$(curl -fsSL https://raw.githubusercontent.com/knadh/listmonk/master/install-prod.sh)"</pre>
<p>Visit <code>http://localhost:9000</code></p>
<p><a href="/docs/installation">Installation docs &rarr;</a></p>
<p class="small">NOTE: Always examine the contents of shell scripts before executing them.</p>
</div>
</div>
</div>
</div>
</section>
<div class="container">
<section class="lists feature">
<h2>Mailing lists</h2>
<div class="center">
<img class="box" src="static/images/lists.png" alt="Screenshot of list management feature" />
</div>
<p>
Manage millions of subscribers across many single and double opt-in lists
with custom JSON attributes for each subscriber.
Query and segment subscribers with SQL expressions.
</p>
<p>Use the fast bulk importer (~10k records per second) or use HTTP/JSON APIs or interact with the simple
table schema to integrate external CRMs and subscriber databases.
</p>
</section>
<section class="tx feature">
<h2>Transactional mails</h2>
<div class="center">
<img class="box" src="static/images/tx.png" alt="Screenshot of transactional API" />
</div>
<p>
Simple API to send arbitrary transactional messages to subscribers
using pre-defined templates. Send messages as e-mail, SMS, Whatsapp messages or any medium via Messenger interfaces.
</p>
</section>
<section class="media feature">
<h2>Analytics</h2>
<div class="center">
<img class="box" src="static/images/analytics.png" alt="Screenshot of analytics feature" />
</div>
<p class="center">
Simple analaytics and visualizations. Connect external visualization programs to the database easily with the simple table structure.
</p>
</section>
<section class="templating feature">
<h2>Templating</h2>
<div class="center">
<img class="box" src="static/images/templating.png" alt="Screenshot of templating feature" />
</div>
<p>
Create powerful, dynamic e-mail templates with the <a href="https://golang.org/pkg/text/template/">Go templating language</a>.
Use template expressions, logic, and 100+ functions in subject lines and content.
Write HTML e-mails in a WYSIWYG editor, Markdown, raw syntax-highlighted HTML, or just plain text.
</p>
</section>
<section class="performance feature">
<h2>Performance</h2>
<div class="center">
<figure class="box">
<img src="static/images/performance.png" alt="Screenshot of performance metrics" />
<figcaption>
A production listmonk instance sending a campaign of 7+ million e-mails.<br />
CPU usage is a fraction of a single core with peak RAM usage of 57 MB.
</figcaption>
</figure>
</div>
<br />
<p>
Multi-threaded, high-throughput, multi-SMTP e-mail queues.
Throughput and sliding window rate limiting for fine grained control.
Single binary application with nominal CPU and memory footprint that runs everywhere.
The only dependency is a Postgres (⩾ v9.4) database.
</p>
</section>
<section class="media feature">
<h2>Media</h2>
<div class="center">
<img class="box" src="static/images/media.png" alt="Screenshot of media feature" />
</div>
<p class="center">Use the media manager to upload images for e-mail campaigns
on the server's filesystem, Amazon S3, or any S3 compatible (Minio) backend.</p>
</section>
<section class="lists feature">
<h2>Extensible</h2>
<div class="center">
<img class="box" src="static/images/messengers.png" alt="Screenshot of Messenger feature" />
</div>
<p class="center">
More than just e-mail campaigns. Connect HTTP webhooks to send SMS,
Whatsapp, FCM notifications, or any type of messages.
</p>
</section>
<section class="privacy feature">
<h2>Privacy</h2>
<div class="center">
<img class="box" src="static/images/privacy.png" alt="Screenshot of privacy features" />
</div>
<p class="center">
Allow subscribers to permanently blocklist themselves, export all their data,
and to wipe all their data in a single click.
</p>
</section>
<h2 class="center">and a lot more &hellip;</h2>
<div class="center">
<br />
<a href="#download" class="button">Download</a>
</div>
<section class="banner">
<div class="row">
<div class="col-2">&nbsp;</div>
<div class="col-8">
<div class="confetti">
<img class="s2" src="static/images/s3.png" />
<div class="box">
<h2>Developers</h2>
<p>
listmonk is free and open source software licensed under AGPLv3.
If you are interested in contributing, check out the <a href="https://github.com/knadh/listmonk">GitHub repository</a>
and refer to the <a href="/docs/developer-setup">developer setup</a>.
The backend is written in Go and the frontend is Vue with Buefy for UI.
</p>
</div>
</div>
</div>
<div class="col-2">&nbsp;</div>
</div>
</section>
</div>
{{ partial "footer.html" }}

View file

@ -0,0 +1,6 @@
{{ partial "header" . }}
<article class="page">
<h1>{{ .Title }}</h1>
{{ .Content }}
</article>
{{ partial "footer" }}

View file

@ -0,0 +1,10 @@
<div class="container">
<footer class="footer">
&copy; 2019-{{ now.Format "2006" }} / <a href="https://nadh.in">Kailash Nadh</a>
</footer>
</div>
<script async defer src="https://buttons.github.io/buttons.js"></script>
</body>
</html>

View file

@ -0,0 +1,42 @@
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>{{ .Title }}</title>
<meta name="description" content="{{with .Description }}{{ . }}{{else}}Send e-mail campaigns and transactional e-mails. High performance and features packed into one app.{{end}}" />
<meta name="keywords" content="{{ if .Keywords }}{{ range .Keywords }}{{ . }}, {{ end }}{{else if isset .Params "tags" }}{{ range .Params.tags }}{{ . }}, {{ end }}{{end}}">
<link rel="canonical" href="{{ .Permalink }}">
<link href="https://fonts.googleapis.com/css?family=Inter:400,600" rel="stylesheet">
<link href="{{ .Site.BaseURL }}static/base.css" rel="stylesheet" type="text/css" />
<link href="{{ .Site.BaseURL }}static/style.css" rel="stylesheet" type="text/css" />
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1" />
<link rel="shortcut icon" href="{{ .Site.BaseURL }}static/images/favicon.png" type="image/x-icon" />
<meta property="og:title" content="{{ .Title }}" />
{{ if .Params.thumbnail }}
<link rel="image_src" href="{{ .Site.BaseURL }}static/images/{{ .Params.thumbnail }}" />
<meta property="og:image" content="{{ .Site.BaseURL }}static/images/{{ .Params.thumbnail }}" />
{{ else }}
<link rel="image_src" href="{{ .Site.BaseURL }}static/images/thumbnail.png" />
<meta property="og:image" content="{{ .Site.BaseURL }}static/images/thumbnail.png" />
{{ end }}
</head>
<body>
<div class="container">
<header class="header">
<div class="row">
<div class="col-2 logo">
<a href="{{ .Site.BaseURL }}"><img src="{{ .Site.BaseURL }}static/images/logo.svg" alt="Listmonk logo" /></a>
</div>
<nav class="col-10">
<a class="item" href="/#download">Download</a>
<a class="item" href="/docs">Docs</a>
<div class="github-btn">
<a class="github-button" href="https://github.com/knadh/listmonk" data-size="large" data-show-count="true" aria-label="knadh/listmonk on GitHub">GitHub</a>
</div>
</nav>
</div>
</header>
</div>

View file

@ -0,0 +1,5 @@
<section class="row">
<div class="col2">&nbsp;</div>
<div class="col8">{{ .Inner }}</div>
<div class="clear"> </div>
</section>

View file

@ -0,0 +1,17 @@
<ul id="github" class="no">
{{ range .Page.Site.Data.github }}
<li class="row">
<div class="col2">
<span class="date">{{ dateFormat "Jan 2006" (substr .updated_at 0 10) }}</span>
</div>
<div class="col3">
<a href="{{ .url }}">{{ .name }}</a>
</div>
<div class="col7 last">
<span class="desc">{{ .description }}</span>
</div>
<div class="clear"> </div>
</li>
{{ end }}
</ul>
<div class="clear"> </div>

View file

@ -0,0 +1,4 @@
<div class="row">
<div class="col7">{{ .Inner }}</div>
<div class="clear"> </div>
</div>

View file

@ -0,0 +1,3 @@
<section>
{{ .Inner }}
</section>

View file

@ -0,0 +1,190 @@
/**
*** SIMPLE GRID
*** (C) ZACH COLE 2016
**/
/* UNIVERSAL */
html,
body {
height: 100%;
width: 100%;
margin: 0;
padding: 0;
left: 0;
top: 0;
font-size: 100%;
}
.right {
text-align: right;
}
.center {
text-align: center;
margin-left: auto;
margin-right: auto;
}
.justify {
text-align: justify;
}
/* ==== GRID SYSTEM ==== */
.container {
margin-left: auto;
margin-right: auto;
}
.row {
position: relative;
width: 100%;
}
.row [class^="col"] {
float: left;
margin: 0.5rem 2%;
min-height: 0.125rem;
}
.col-1,
.col-2,
.col-3,
.col-4,
.col-5,
.col-6,
.col-7,
.col-8,
.col-9,
.col-10,
.col-11,
.col-12 {
width: 96%;
}
.col-1-sm {
width: 4.33%;
}
.col-2-sm {
width: 12.66%;
}
.col-3-sm {
width: 21%;
}
.col-4-sm {
width: 29.33%;
}
.col-5-sm {
width: 37.66%;
}
.col-6-sm {
width: 46%;
}
.col-7-sm {
width: 54.33%;
}
.col-8-sm {
width: 62.66%;
}
.col-9-sm {
width: 71%;
}
.col-10-sm {
width: 79.33%;
}
.col-11-sm {
width: 87.66%;
}
.col-12-sm {
width: 96%;
}
.row::after {
content: "";
display: table;
clear: both;
}
.hidden-sm {
display: none;
}
@media only screen and (min-width: 33.75em) { /* 540px */
.container {
width: 80%;
}
}
@media only screen and (min-width: 45em) { /* 720px */
.col-1 {
width: 4.33%;
}
.col-2 {
width: 12.66%;
}
.col-3 {
width: 21%;
}
.col-4 {
width: 29.33%;
}
.col-5 {
width: 37.66%;
}
.col-6 {
width: 46%;
}
.col-7 {
width: 54.33%;
}
.col-8 {
width: 62.66%;
}
.col-9 {
width: 71%;
}
.col-10 {
width: 79.33%;
}
.col-11 {
width: 87.66%;
}
.col-12 {
width: 96%;
}
.hidden-sm {
display: block;
}
}
@media only screen and (min-width: 60em) { /* 960px */
.container {
width: 75%;
max-width: 60rem;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 360 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 372 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -0,0 +1,233 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="45.041653mm"
height="9.8558731mm"
viewBox="0 0 45.041653 9.8558733"
version="1.1"
id="svg8"
sodipodi:docname="listmonk.src.svg"
inkscape:version="1.0 (9f2f71dc58, 2020-08-02)">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="1"
inkscape:cx="742.82396"
inkscape:cy="-93.302628"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1863"
inkscape:window-height="1025"
inkscape:window-x="57"
inkscape:window-y="27"
inkscape:window-maximized="1"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:document-rotation="0" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-12.438455,-21.535559)">
<path
style="fill:#ffcc00;fill-opacity:1;stroke:none;stroke-width:2.11094689;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 16.660914,21.535559 a 4.2220837,4.2220837 0 0 0 -4.222459,4.222437 4.2220837,4.2220837 0 0 0 0.490699,1.968681 c 0.649637,-1.386097 2.059696,-2.343758 3.73176,-2.343758 1.672279,0 3.082188,0.958029 3.731731,2.344413 a 4.2220837,4.2220837 0 0 0 0.490039,-1.969336 4.2220837,4.2220837 0 0 0 -4.22177,-4.222437 z"
id="circle920"
inkscape:connector-curvature="0"
inkscape:export-filename="/home/kailash/Site/listmonk/static/logo.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96" />
<flowRoot
xml:space="preserve"
id="flowRoot935"
style="font-style:normal;font-weight:normal;font-size:40px;line-height:1.25;font-family:sans-serif;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
transform="matrix(0.27888442,0,0,0.27888442,92.852428,101.67857)"><flowRegion
id="flowRegion937"><rect
id="rect939"
width="338"
height="181"
x="-374"
y="-425.36423" /></flowRegion><flowPara
id="flowPara941" /></flowRoot>
<text
id="text874-8"
y="30.29347"
x="23.133614"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:8.70789px;line-height:1.25;font-family:'IBM Plex Sans';-inkscape-font-specification:'IBM Plex Sans, Medium';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.0459737"
xml:space="preserve"
inkscape:export-filename="/home/kailash/Site/listmonk/static/logo.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"><tspan
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:8.70789px;font-family:Inter;-inkscape-font-specification:'Inter Semi-Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;writing-mode:lr-tb;text-anchor:start;stroke-width:0.0459737"
y="30.29347"
x="23.133614"
id="tspan872-0"
sodipodi:role="line">listmonk</tspan></text>
<circle
r="3.1873188"
cy="27.647591"
cx="16.66629"
id="circle876-1"
style="fill:none;fill-opacity:1;stroke:#7f2aff;stroke-width:1.11304522;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
inkscape:export-filename="/home/kailash/Site/listmonk/static/logo.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96" />
<path
inkscape:connector-curvature="0"
id="path878-0"
d="m 16.666291,24.813242 a 3.1873187,3.8372081 0 0 0 -3.187196,3.837044 3.1873187,3.8372081 0 0 0 0.07347,0.79818 3.1873187,3.8372081 0 0 1 3.113724,-3.027362 3.1873187,3.8372081 0 0 1 3.113721,3.038883 3.1873187,3.8372081 0 0 0 0.07347,-0.809701 3.1873187,3.8372081 0 0 0 -3.187196,-3.837044 z"
style="fill:#7f2aff;fill-opacity:1;stroke:none;stroke-width:1.22125876;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
inkscape:export-filename="/home/kailash/Site/listmonk/static/logo.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96" />
<path
style="fill:#ffcc00;fill-opacity:1;stroke:none;stroke-width:1.06017;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 139.94612,-53.327122 a 2.1703097,2.0716912 0 0 0 -2.17051,2.071864 2.1703097,2.0716912 0 0 0 0.25224,0.965993 c 0.33394,-0.680131 1.05876,-1.150035 1.91827,-1.150035 0.85961,0 1.58436,0.470085 1.91825,1.150356 a 2.1703097,2.0716912 0 0 0 0.2519,-0.966314 2.1703097,2.0716912 0 0 0 -2.17015,-2.071864 z"
id="path1200"
inkscape:connector-curvature="0"
inkscape:export-filename="/home/kailash/Site/listmonk/static/logo.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96" />
<text
id="text1204"
y="-46.771812"
x="116.91617"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:8.70789px;line-height:1.25;font-family:'IBM Plex Sans';-inkscape-font-specification:'IBM Plex Sans, Medium';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.0459737"
xml:space="preserve"
inkscape:export-filename="/home/kailash/Site/listmonk/static/logo.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"><tspan
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:8.70789px;font-family:Inter;-inkscape-font-specification:'Inter Semi-Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;writing-mode:lr-tb;text-anchor:start;stroke-width:0.0459737"
y="-46.771812"
x="116.91617"
id="tspan1202"
sodipodi:role="line">listmonk</tspan></text>
<text
id="text1214"
y="-23.851294"
x="127.87717"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:6.82489px;line-height:1.25;font-family:'IBM Plex Sans';-inkscape-font-specification:'IBM Plex Sans, Medium';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.0360324"
xml:space="preserve"
inkscape:export-filename="/home/kailash/Site/listmonk/static/logo.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"><tspan
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:6.82489px;font-family:Inter;-inkscape-font-specification:'Inter Semi-Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;writing-mode:lr-tb;text-anchor:start;stroke-width:0.0360324"
y="-23.851294"
x="127.87717"
id="tspan1212"
sodipodi:role="line">listmonk</tspan></text>
<circle
style="fill:#ffcc00;fill-opacity:1;stroke:none;stroke-width:2.5729"
id="path1216"
cx="203.43507"
cy="-21.854498"
r="3.8091576" />
<g
id="g1239"
transform="matrix(1.2398232,0,0,1.2398232,25.599078,-34.522694)">
<rect
style="fill:#7f2aff;fill-opacity:1;stroke:none;stroke-width:0.91563"
id="rect1218"
width="3.7532511"
height="0.89233136"
x="77.048592"
y="4.6184554" />
<rect
style="fill:#7f2aff;fill-opacity:1;stroke:none;stroke-width:0.91563"
id="rect1220"
width="3.7532511"
height="0.89233136"
x="77.048592"
y="6.1939058" />
<rect
style="fill:#7f2aff;fill-opacity:1;stroke:none;stroke-width:0.91563"
id="rect1226"
width="3.7532511"
height="0.89233136"
x="77.048592"
y="7.7760162" />
</g>
<ellipse
style="fill:#ffcc00;fill-opacity:1;stroke:none;stroke-width:1.5875"
id="path1247"
cx="139.2197"
cy="-74.271935"
rx="2.1283948"
ry="1.9833959" />
<text
id="text1245"
y="-71.648537"
x="115.96989"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:8.70789px;line-height:1.25;font-family:'IBM Plex Sans';-inkscape-font-specification:'IBM Plex Sans, Medium';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.0459737"
xml:space="preserve"
inkscape:export-filename="/home/kailash/Site/listmonk/static/logo.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"><tspan
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:8.70789px;font-family:Inter;-inkscape-font-specification:'Inter Semi-Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;writing-mode:lr-tb;text-anchor:start;stroke-width:0.0459737"
y="-71.648537"
x="115.96989"
id="tspan1243"
sodipodi:role="line">listmonk</tspan></text>
<text
id="text1042"
y="-18.770809"
x="210.12352"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:8.70789px;line-height:1.25;font-family:'IBM Plex Sans';-inkscape-font-specification:'IBM Plex Sans, Medium';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-0.0529167px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.0459737"
xml:space="preserve"
inkscape:export-filename="/home/kailash/Site/listmonk/static/logo.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"><tspan
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:8.70789px;font-family:'Fira Sans';-inkscape-font-specification:'Fira Sans Semi-Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;writing-mode:lr-tb;text-anchor:start;stroke-width:0.0459737"
y="-18.770809"
x="210.12352"
id="tspan1040"
sodipodi:role="line">listmonk</tspan></text>
<circle
style="fill:none;fill-opacity:1;stroke:#ffcc00;stroke-width:1.73982;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="circle1737"
cx="203.74388"
cy="-1.1837244"
r="3.1489604" />
<text
id="text1741"
y="2.24283"
x="210.38811"
style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:8.70789px;line-height:1.25;font-family:'IBM Plex Sans';-inkscape-font-specification:'IBM Plex Sans, Medium';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:-0.0529167px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.0459737"
xml:space="preserve"
inkscape:export-filename="/home/kailash/Site/listmonk/static/logo.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96"><tspan
style="font-style:normal;font-variant:normal;font-weight:600;font-stretch:normal;font-size:8.70789px;font-family:'Fira Sans';-inkscape-font-specification:'Fira Sans Semi-Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;writing-mode:lr-tb;text-anchor:start;stroke-width:0.0459737"
y="2.24283"
x="210.38811"
id="tspan1739"
sodipodi:role="line">listmonk</tspan></text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="163.03" height="30.38" viewBox="0 0 43.135 8.038" xmlns:v="https://vecta.io/nano"><circle cx="4.019" cy="4.019" r="3.149" fill="none" stroke="#0055d4" stroke-width="1.74"/><path d="M11.457 7.303q-.566 0-.879-.322-.313-.331-.313-.932V.712L11.5.572v5.442q0 .305.253.305.139 0 .244-.052l.253.879q-.357.157-.792.157zm2.619-4.754v4.615H12.84V2.549zM13.449.172q.331 0 .54.209.218.2.218.514 0 .313-.218.522-.209.2-.54.2-.331 0-.54-.2-.209-.209-.209-.522 0-.313.209-.514.209-.209.54-.209zm3.319 2.238q.975 0 1.672.557l-.47.705q-.583-.366-1.149-.366-.305 0-.47.113-.165.113-.165.305 0 .139.07.235.078.096.279.183.209.087.618.209.731.2 1.088.54.357.331.357.914 0 .462-.27.801-.261.34-.714.522-.453.174-1.01.174-.583 0-1.062-.174-.479-.183-.819-.496l.61-.679q.583.453 1.237.453.348 0 .549-.131.209-.139.209-.374 0-.183-.078-.287-.078-.104-.287-.192-.209-.096-.653-.218-.697-.192-1.036-.54-.331-.357-.331-.879 0-.392.226-.705.226-.313.636-.488.418-.183.967-.183zm5.342 4.536q-.253.174-.575.261-.313.096-.627.096-.714-.009-1.08-.409-.366-.401-.366-1.176V3.42h-.688v-.871h.688v-1.01l1.237-.148v1.158h1.062l-.122.871h-.94v2.273q0 .331.113.479.113.148.348.148.235 0 .522-.157zm5.493-4.536q.549 0 .879.374.34.374.34 1.019v3.361h-1.237V4.012q0-.679-.453-.679-.244 0-.427.157-.183.157-.374.488v3.187h-1.237V4.012q0-.679-.453-.679-.244 0-.427.165-.183.157-.366.479v3.187h-1.237V2.549h1.071l.096.575q.261-.348.583-.531.331-.183.758-.183.392 0 .679.2.287.192.418.549.287-.374.618-.557.34-.192.766-.192zm4.148 0q1.036 0 1.62.653.583.644.583 1.794 0 .731-.27 1.289-.261.549-.766.853-.496.305-1.176.305-1.036 0-1.628-.644-.583-.653-.583-1.803 0-.731.261-1.28.27-.557.766-.862.505-.305 1.193-.305zm0 .923q-.47 0-.705.374-.226.366-.226 1.149 0 .784.226 1.158.235.366.697.366.462 0 .688-.366.235-.374.235-1.158 0-.784-.226-1.149-.226-.374-.688-.374zm5.271-.923q.61 0 .949.374.34.366.34 1.019v3.361h-1.237V4.012q0-.374-.131-.522-.122-.157-.374-.157-.261 0-.479.165-.209.157-.409.479v3.187h-1.237V2.549h1.071l.096.583q.287-.357.627-.54.348-.183.784-.183zM40.2.572v6.592h-1.237V.712zm2.804 1.977l-1.472 2.029 1.602 2.586h-1.402l-1.489-2.525 1.48-2.09z"/></svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 506 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 623 B

View file

@ -0,0 +1,83 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="svg1065"
width="21.1164"
height="17.646732"
viewBox="0 0 21.1164 17.646732"
sodipodi:docname="s2.svg"
inkscape:export-filename="/home/kailash/www/listmonk/site/static/static/images/s2.png"
inkscape:export-xdpi="115.86"
inkscape:export-ydpi="115.86"
inkscape:version="1.0.2 (e86c870879, 2021-01-15)">
<metadata
id="metadata1071">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs1069">
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect1698"
is_visible="true"
lpeversion="1"
satellites_param="F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1 @ F,0,0,1,0,0,0,1"
unit="px"
method="auto"
mode="IC"
radius="10"
chamfer_steps="1"
flexible="false"
use_knot_distance="false"
apply_no_radius="false"
apply_with_radius="false"
only_selected="false"
hide_knots="false" />
</defs>
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1007"
id="namedview1067"
showgrid="false"
inkscape:zoom="4"
inkscape:cx="28.817195"
inkscape:cy="14.597549"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="g1073" />
<g
inkscape:groupmode="layer"
inkscape:label="Image"
id="g1073"
transform="translate(-4.4667969,-5.166384)">
<path
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-variant-east-asian:normal;font-feature-settings:normal;font-variation-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;shape-margin:0;inline-size:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#0055d4;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:normal;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate;stop-color:#000000;stop-opacity:1"
d="M 15.923828,5.3164062 C 11.592147,4.6111791 7.0486038,6.4161348 4.4667969,10.228516 c 0,0.53125 0.060547,2.755859 4.1230469,3.224609 1.7958472,-2.651806 5.8189972,-3.5264183 8.5820312,-1.822266 2.837636,1.750166 3.699383,5.385019 1.949219,8.222657 -0.4375,1.71875 2.066406,3.18164 4.753906,2.93164 C 27.209467,17.378802 25.509869,10.211423 20.103516,6.8769531 18.787461,6.0652517 17.367722,5.551482 15.923828,5.3164062 Z"
id="path1671"
sodipodi:nodetypes="sccsccss" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View file

@ -0,0 +1,280 @@
body {
background: #fdfdfd;
font-family: "Inter", "Helvetica Neue", "Segoe UI", sans-serif;
font-size: 17px;
font-weight: 400;
line-height: 30px;
color: #444;
overflow-x: hidden;
}
h1,
h2,
h3,
h4,
h5 {
font-weight: 600;
margin: 5px 0 15px 0;
color: #111;
}
h1 {
font-size: 2.5em;
line-height: 1.2em;
letter-spacing: -0.01em;
}
h2 {
font-size: 2em;
line-height: 1.4em;
}
h3 {
font-size: 1.6em;
line-height: 1.6em;
}
strong {
font-weight: 600;
}
section:not(:last-child) {
margin-bottom: 100px;
}
a {
color: #0055d4;
text-decoration: none;
}
a:hover {
color: #111;
}
::selection {
background: #111;
color: #fff;
}
pre {
background: #fafafa;
padding: 5px;
border-radius: 3px;
overflow-x: scroll;
}
code {
background: #fafafa;
padding: 5px;
border-radius: 3px;
}
img {
max-width: 100%;
}
/* Helpers */
.center {
text-align: center;
}
.small, code, pre {
font-size: 13px;
line-height: 20px;
color: #333;
}
.box {
background: #fff;
border-radius: 6px;
border: 1px solid #e6e6e6;
box-shadow: 1px 1px 4px #e6e6e6;
padding: 30px;
}
img.box {
display: inline-block;
padding: 0;
}
figcaption {
color: #888;
font-size: 0.9em;
}
.button {
background: #0055d4;
display: inline-block;
text-align: center;
font-weight: 600;
color: #fff;
border-radius: 100px;
padding: 10px 15px;
min-width: 150px;
}
.button:hover {
background: #111;
color: #fff;
}
.notice {
background: #fafafa;
border-left: 4px solid #ddd;
color: #666;
padding: 5px 15px;
}
/* Layout */
.container {
max-width: 1200px;
margin: 0 auto;
}
.header {
margin: 30px 0 60px 0;
text-align: left;
}
.logo img {
width: 125px;
height: auto;
}
nav {
text-align: right;
}
nav .item:not(:first-child) {
margin: 0 0 0 40px;
}
.github-btn {
min-width: 135px;
min-height: 38px;
float: right;
margin-left: 30px;
}
.splash .hero {
margin-bottom: 60px;
}
.splash .title {
max-width: 700px;
margin: 0 auto 30px auto;
font-size: 3em;
}
.splash .sub {
font-weight: 400;
color: #666;
}
.splash .confetti {
max-width: 1000px;
margin: 0 auto;
}
.splash .demo {
margin-top: 30px;
}
.confetti {
position: relative;
}
.confetti .s1, .confetti .s2, .confetti .s3 {
position: absolute;
}
.confetti.light .s1, .confetti.light .s2, .confetti.light .s3 {
opacity: 0.30;
}
.confetti .s1 {
left: -35px;
top: 20%;
z-index: 10;
}
.confetti .s2 {
z-index: 30;
right: 20%;
top: -12px;
}
.confetti .s3 {
z-index: 30;
left: 15%;
bottom: 0;
}
.confetti .box {
position: relative;
z-index: 20;
}
#download {
background: #f9f9f9;
padding: 160px 0 90px 0;
margin-top: -90px;
}
#download .install-steps li {
margin-bottom: 15px;
}
#download .download-links a:not(:last-child) {
display: inline-block;
margin-right: 15px;
}
#download .box {
min-height: 630px;
}
.feature {
}
.feature h2 {
margin-bottom: 1em;
text-align: center;
}
.feature img {
margin-bottom: 1em;
}
.feature p {
margin-left: auto;
margin-right: auto;
max-width: 750px;
}
.banner {
padding-top: 90px;
}
.footer {
border-top: 1px solid #eee;
padding-top: 30px;
margin: 90px 0 30px 0;
color: #777;
}
@media screen and (max-width: 720px) {
body {
/*font-size: 16px;*/
}
.header {
margin-bottom: 15px;
text-align: center;
}
.header .columns {
margin-bottom: 10px;
}
.box {
padding: 15px;
}
.splash .title {
font-size: 2.1em;
line-height: 1.3em;
}
.splash .sub {
font-size: 1.3em;
line-height: 1.5em;
}
nav {
text-align: center;
}
.github-btn {
float: none;
margin: 15px 0 0 0;
}
section:not(:last-child) {
margin-bottom: 45px;
}
}
@media screen and (max-width: 540px) {
.container {
padding: 0 15px;
}
}