Postal: Transaction Email In Nethserver

Edit: this Feature request became long, but informative

As with any Business that has a presence online, or looking to build an online presence, sending bulk emails is very key and paramount.
This is Mostly achieved through building an email, and is done through the use of Email marketing softwires.

Commercial.
The commercial vendors of Such a systems include Mailchimp, Hubspot, aweber (this is really old) and others.
Self-Hosted(Open source)
Notably there exists also open source versions of the software, the most popular being
https://www.mautic.org/

https://acellemail.com/
and

Transaction EMail Server
All these methods and solutions make use of and require a transaction email server,
the most commercial ones, are the likes of
sendgrid, pepipost, elasticmail, zeptomail and others.

Postal Server
A small lightweight self hosted Transaction Email solution. owned by Krystal
it can be deployed with docker, has ip rotation built-in as well as tracking functionalities for emails send.

The Installation Instructions are found here:

here is the GitHub page for the same

A few scenarios worth considering

  1. Since its a mail server, it probably makes use of some ports already used by nethserver-mail.
    What if mail module has not been installed, can this be installed and make use of the same ports without an issue.

  2. Can it be installed to run alongside netherver mail?
    @mrmarkuz mentioned that for used ports, a different network thats not aqua can be used, or alternatively another port on the aqua network.

  3. can it run alongside Nethserver mail with no real repercussions.

For me, since i have multiple nethserver deployments spread across the internet, that do not use or have the need for Nethserver mail to be installed, i have always wanted a way to utilize those servers for delivering emails, while rotating the delivery servers based on the emails being sent hourly.
As we all know, if a single ip sends way too many emails, it becomes backlisted.

The software can solve that through ip rotation, but then again, the idle nethservers can also be utilized.

@stephdl a while back you presented us with an email list software, here is a transactional email server to go with it.

Hi @oneitonitram

After I finished an how-to, sometimes I send a newsletter about it from WordPress using MailPoet which is free: MailPoet Newsletters (Previous) – WordPress plugin | WordPress.org,
https://www.mailpoet.com/.

You can schedule the sending and how many by batch.
If you install a minimum WordPress and MailPoet, you should be fine.

With Discourse, I use Sparkport: Email Delivery Service Pricing | SparkPost
It’s free if you have less than 300 emails/month, $20/month for a 1K emails, $30/month for 25K emails.

Michel-André

those are good options. i think elastic email is even more generous than sparkpost. even pepipost has more emails than sparkpost, but everyone has their preferences.

This is why havin gyou own, will enable you to save alot. the average cost of a vm is about $5 per month, which gives you a dedicated ip which will enable you to send as many as need be

<mode troll on>
when you want to build a business online you cannot count on my help, except if you are ready to pay me a full time job

You cannot rely on free code for that purpose :smiley:
<mode troll off/>

3 Likes

nothing is ever free anyway, at some point you’ll have to pay, but its always a beginning point for small projects.

Overall it was a back story, and i post things i find usefule, that i would and could use, as well as others that other pople might find useful and can use.

1 Like

That’s true in french we say

Un programme devient libre qu’une fois qu’il a ete payé.

1 Like

Hello,

Through the Immense Support of @Shane_Treweek and @mrmarkuz
we now have a Module for Postal.
Available here: mrmarkuz/nethserver-postal (github.com)

the Following DNS records would be required to have it up and running properly

A Record

postal.$domain A $IP

MX Record

@ MX 10 postal.$domain

SPF Record

spf.postal.$domain TXT v=spf1 ip4:$IP ~all

Return Path

rp.postal.$domain A $IP

rp.postal.$domain TXT v=spf1 a mx include:spf.postal.$domain ~all

postal._domainkey.rp.postal.$domain TXT Value from 'postal default-dkim-record'

Route Domain

routes.postal.$domain MX 10 postal.$domain

So that it reflects this in postal.yml

dns:
  mx_records:
    - postal.$domain
  smtp_server_hostname: postal.$domain
  track_domain: postal.$domain
  spf_include: spf.postal.$domain
  return_path: rp.postal.$domain
  route_domain: routes.postal.$domain

Here is Simple Installation:

yum -y install https://mrmarkuz.dynu.net/mirror/devtest/nethserver-postal-1.0.0-1.ns7.noarch.rpm

MAybe @Shane_Treweek could share the Postalinstall.sh script. too.

Thank you.

2 Likes
Postalinstall.sh
#!/usr/bin/env bash

echo "Hello, Lets install some prerequisites (git, curl, jq and docker-compose)."
sleep 0.2s
echo -e

yum -y install git curl jq nethserver-firewall-base-ui nethserver-docker php-pecl-memcached php-imap php-mysql php-mcrypt php-pecl-redis php-pecl-apcu php-smbclient php-ldap php-mbstring
wait
echo -e
echo "now lets open up the necessary firewall ports"
echo -e

config set rabbitmq service status enabled TCPPorts 5672 UDPPorts 5672 access red,green

config set postalweb service status enabled TCPPorts 5000 UDPPorts 5000 access red,green

config set dnspostal service status enabled UDPPorts 53 access red,green

config set smtps service status enabled TCPPorts 465 UDPPorts 465 access red,green ##change to port you want if it conflicts

config set imap service status enabled TCPPorts 143 UDPPorts 143 access red,green ##change to port you want if it conflicts

config set imaps service status enabled TCPPorts 993 UDPPorts 993 access red,green ##change to port you want if it conflicts

config set postal-mariadb service status enabled TCPPorts 3307 UDPPorts 3307 access red,green

signal-event firewall-adjust

wait

echo -e
echo "Ok Done."
echo -e
sleep 0.2s

echo -e
echo "Please wait while I build docker-compose from source."
echo -e
sleep 0.2s

sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
sudo ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose

wait

echo -e
echo "Ok Done."
echo -e
sleep 0.2s

echo -e
echo "Now I'll setup the helper repository."
sleep 0.2s
echo -e

git clone https://postalserver.io/start/install /opt/postal/install
sudo ln -s /opt/postal/install/bin/postal /usr/bin/postal

wait

echo "<VirtualHost *:443>
    ServerName postal.$domain
    RewriteEngine On
    SSLEngine On
    ProxyPassMatch ^/.well-known/acme-challenge/ !
    RequestHeader set X-Forwarded-Proto "https"
    ProxyPass / http://172.17.0.1:5000/
    ProxyPassReverse / http://172.17.0.1:5000/
    ProxyPreserveHost On
    ProxyErrorOverride Off
</VirtualHost>

<VirtualHost *:80>
    ServerName postal.$domain
    RewriteEngine On
    ProxyPassMatch ^/.well-known/acme-challenge/ !
    RewriteCond %{HTTPS} !=on
    RewriteRule (.*) https://%{SERVER_NAME}$1 [R,L]
</VirtualHost>" > /etc/httpd/conf.d/postal.conf

systemctl restart httpd
echo -e
echo "Ok Done."
echo -e
sleep 0.2s

head /dev/urandom | tr -dc A-Za-z0-9 | head -c10 >> ~/pass.txt

echo -e
echo "Thank you now I'll install the rabbitmq instance on docker."
echo -e
sleep 0.2s

docker run -d \
   --name postal-rabbitmq \
   -p 127.0.0.1:5672:5672 \
   --restart always \
   -e RABBITMQ_DEFAULT_USER=postal \
   -e RABBITMQ_DEFAULT_PASS=$(cat ~/pass.txt) \
   -e RABBITMQ_DEFAULT_VHOST=postal \
   rabbitmq:3.8

wait

echo -e
echo "Thank you please wait while I create your database and credentials this may take a moment"
echo -e
sleep 0.2s

docker run -d \
   --name postal-mariadb \
   -p 127.0.0.1:3307:3306 \
   --restart always \
   -e MARIADB_DATABASE=postal \
   -e MARIADB_ROOT_PASSWORD=$(cat ~/pass.txt) \
   mariadb

wait
echo -e
echo "Ok Done."
echo -e
sleep 0.2s

echo -e
echo  "What is your domain?"
echo -e
read -r domain
sleep 0.2s

postal bootstrap postal.$domain

wait

echo -e
echo "Thank you please wait while I initialize your postal this may take a moment"
echo -e
sleep 0.2s

sed -i '4s/  reverse_proxy .*/  reverse_proxy 172.17.0.1:5000/' /opt/postal/config/Caddyfile;
sed -i '3s/  use_ip_pools: .*/  use_ip_pools: true/' /opt/postal/config/postal.yml;
sed -i '13s/  bind_address: .*/  bind_address: 172.17.0.1/' /opt/postal/config/postal.yml;
sed -i '18s/  port: .*/  port: 465/' /opt/postal/config/postal.yml;
sed -i "41s/host: .*/host: $(docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' `docker ps -aqf "name=postal-rabbitmq"`)/g" /opt/postal/config/postal.yml;
sed -i "26s/host: .*/host: $(docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' `docker ps -aqf "name=postal-mariadb"`)/g" /opt/postal/config/postal.yml;
sed -i '52s/smtp_server_hostname: .*/smtp_server_hostname: '"postal.$domain"'/g' /opt/postal/config/postal.yml;
sed -i "34s/host: .*/host: $(docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' `docker ps -aqf "name=postal-mariadb"`)/g" /opt/postal/config/postal.yml;
sed -i '56s/track_domain: .*/track_domain: '"track.postal.$domain"'/g' /opt/postal/config/postal.yml;
sed -i '55s/route_domain: .*/route_domain: '"routes.postal.$domain"'/g' /opt/postal/config/postal.yml;
sed -i '54s/return_path: .*/return_path: '"return.postal.$domain"'/g' /opt/postal/config/postal.yml;
sed -i '53s/spf_include: .*/spf_include: '"spf.postal.$domain"'/g' /opt/postal/config/postal.yml;
sed -i '51s/  - .*/  - '"postal.$domain"'/g' /opt/postal/config/postal.yml;
sed -i "43s/password: .*/password: $(cat ~/pass.txt)/g" /opt/postal/config/postal.yml;
sed -i "28s/password: .*/password: $(cat ~/pass.txt)/g" /opt/postal/config/postal.yml;
sed -i "36s/password: .*/password: $(cat ~/pass.txt)/g" /opt/postal/config/postal.yml;



postal initialize

wait

postal make-user

wait

echo -e
echo "Database setup, initialised and user setup successfully please wait while I start Postal"
echo -e
sleep 0.2s

postal start

wait


echo -e
echo "Ok Done."
echo -e
sleep 0.2s

echo -e
echo "Now I'll create a script to clean and automate the start of Postal on reboot"
echo -e
sleep 0.2s

echo "#!/bin/bash
PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin
LD_LIBRARY_PATH=/usr/local/lib
/usr/bin/postal status | grep Exit && date >> ~/logpostal.log
/usr/bin/postal status | grep Exit && /usr/bin/postal start" > ~/postalCheck.sh

echo -e
echo "Ok Done, now lets make the script executable."
echo -e
sleep 0.2s

chmod +x ~/postalCheck.sh

echo -e
echo "Ok Done, now I'll add it to crontab."
echo -e
sleep 0.2s

echo "@reboot sleep 60 && ~/postalCheck.sh" >> /etc/crontab

echo -e
echo "Ok Done."
echo -e
sleep 0.2s



echo -e
echo "Thank you for your patience, please open a web browser to https://postal.$domain"
echo "Please take note of your mariadb and rabbitmq passwd $(cat pass.txt) and press enter so it can be deleted for security"
wait
echo "the password file will now be deleted"
rm -f ~/pass.txt

heres a script to automate creating all the records required for postal with cloudflare

cfdnsrecords.sh
#!/usr/bin/env bash

echo -e
echo  "What is your API token ?"
echo -e
read -r token
sleep 0.2s

echo -e
echo  "What is the domain name?"
echo -e
read -r domain

echo -e
echo  "What is Public IP address?"
echo -e
read -r IP

wait

postal default-dkim-record > ~/dkim.txt

curl -X GET "https://api.cloudflare.com/client/v4/zones" \
    -H "Authorization: Bearer $token" \
    -H "Content-Type: application/json" \
    | python -c $'import sys,json\ndata=json.loads(sys.stdin.read())\nif data["success"]:\n\tfor dict in data["result"]:print("Zone ID: " + dict["id"])\nelse:print("ERROR(" + str(data["errors"][0]["code"]) + "): " + data["errors"][0]["message"])' > zoneid.txt

wait

curl -X POST "https://api.cloudflare.com/client/v4/zones/$(sed 's/Zone ID: //' zoneid.txt)/dns_records/" \
    -H "Authorization: Bearer $token" \
-H "Content-Type: application/json" \
--data '{"type":"'"A"'","name":"'"postal.$domain"'","content":"'"$IP"'","proxied":'"false"',"ttl":'"1"'}' \
| python -m json.tool;

wait

curl -X POST "https://api.cloudflare.com/client/v4/zones/$(sed 's/Zone ID: //' zoneid.txt)/dns_records/" \
    -H "Authorization: Bearer $token" \
-H "Content-Type: application/json" \
    --data '{"type":"'"MX"'","name":"'"@"'","content":"'"postal.$domain"'","priority":'"10"',"ttl":'"1"'}' \
| python -m json.tool;

wait

 curl -X POST "https://api.cloudflare.com/client/v4/zones/$(sed 's/Zone ID: //' zoneid.txt)/dns_records/" \
    -H "Authorization: Bearer $token" \
-H "Content-Type: application/json" \
    --data '{"type":"'"TXT"'","name":"'"spf.postal.$domain"'","content":"'"v=spf1 ip4:$IP ~all"'","ttl":'"1"'}' \
| python -m json.tool;

wait

curl -X POST "https://api.cloudflare.com/client/v4/zones/$(sed 's/Zone ID: //' zoneid.txt)/dns_records/" \
    -H "Authorization: Bearer $token" \
-H "Content-Type: application/json" \
--data '{"type":"'"A"'","name":"'"rp.postal.$domain"'","content":"'"$IP"'","proxied":'"false"',"ttl":'"1"'}' \
| python -m json.tool;

wait

# curl -X POST "https://api.cloudflare.com/client/v4/zones/$(sed 's/Zone ID: //' zoneid.txt)/dns_records/" \
 #   -H "Authorization: Bearer $token" \
#-H "Content-Type: application/json" \
 #   --data '{"type":"'"TXT"'","name":"'"rp.postal.$domain"'","content":"'"	v=spf1 a mx include:spf.postal.$domain ~all"'","ttl":'"1"'}' \
#| python -m json.tool;
curl -X POST "https://api.cloudflare.com/client/v4/zones/$(sed 's/Zone ID: //' zoneid.txt)/dns_records/" \
    -H "Authorization: Bearer $token" \
-H "Content-Type: application/json" \
    --data '{"type":"'"TXT"'","name":"'"rp.postal.$domain"'","content":"'"v=spf1 a mx include:spf.postal.$domain ~all"'","ttl":'"1"'}' \
| python -m json.tool;

wait

 curl -X POST "https://api.cloudflare.com/client/v4/zones/$(sed 's/Zone ID: //' zoneid.txt)/dns_records/" \
    -H "Authorization: Bearer $token" \
-H "Content-Type: application/json" \
#    --data '{"type":"'"TXT"'","name":"'"postal._domainkey.rp.postal.$domain"'","content":"'"'$(cat ~/dkim.txt)'"'"1"'}' \
 --data '{"type":"'"TXT"'","name":"'"postal._domainkey.rp.postal.$domain"'","content":"'"$(cat dkim.txt)"'","ttl":'"1"'}' \
| python -m json.tool;

wait

curl -X POST "https://api.cloudflare.com/client/v4/zones/$(sed 's/Zone ID: //' zoneid.txt)/dns_records/" \
    -H "Authorization: Bearer $token" \
-H "Content-Type: application/json" \
    --data '{"type":"'"MX"'","name":"'"postal.$domain"'","content":"'"routes.postal.$domain"'","priority":'"10"',"ttl":'"1"'}' \
| python -m json.tool;
1 Like