<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>WebDev - NET73</title>
	<atom:link href="https://net73.de/category/linux/webdev/feed/" rel="self" type="application/rss+xml" />
	<link>https://net73.de</link>
	<description>Techblog, Freizeitblog &#38; Webhosting</description>
	<lastBuildDate>Sat, 15 Nov 2025 22:19:26 +0000</lastBuildDate>
	<language>de</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.9.4</generator>

<image>
	<url>https://net73.de/wp-content/uploads/2025/11/favicon512-150x150.png</url>
	<title>WebDev - NET73</title>
	<link>https://net73.de</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>Automatisches DNS-Setup mit Cloudflare- und Mailcow-API</title>
		<link>https://net73.de/automatisches-dns-setup-mit-cloudflare-und-mailcow-api/?utm_source=rss&#038;utm_medium=rss&#038;utm_campaign=automatisches-dns-setup-mit-cloudflare-und-mailcow-api</link>
					<comments>https://net73.de/automatisches-dns-setup-mit-cloudflare-und-mailcow-api/#respond</comments>
		
		<dc:creator><![CDATA[Marc Eggert]]></dc:creator>
		<pubDate>Sat, 15 Nov 2025 22:19:25 +0000</pubDate>
				<category><![CDATA[Linux]]></category>
		<category><![CDATA[WebDev]]></category>
		<category><![CDATA[Docker]]></category>
		<category><![CDATA[linux]]></category>
		<category><![CDATA[mail]]></category>
		<category><![CDATA[mailcow]]></category>
		<category><![CDATA[postfix]]></category>
		<guid isPermaLink="false">https://net73.de/?p=3685</guid>

					<description><![CDATA[<p>Wenn du mehrere Domains mit Mailcow betreibst, wiederholt sich immer derselbe Nervkram: MX setzen, SPF schreiben, DKIM eintragen, DMARC hinterher, Autoconfig und Co. nicht vergessen. Weil ich darauf keine Lust mehr hatte, erledigt das bei mir jetzt ein Bash-Script, das: In diesem Beitrag zeige ich dir: Am Ende findest du das komplette Script und die [&#8230;]</p>
<p>The post <a href="https://net73.de/automatisches-dns-setup-mit-cloudflare-und-mailcow-api/">Automatisches DNS-Setup mit Cloudflare- und Mailcow-API</a> first appeared on <a href="https://net73.de">NET73</a>.</p>]]></description>
										<content:encoded><![CDATA[<p>Wenn du mehrere Domains mit Mailcow betreibst, wiederholt sich immer derselbe Nervkram: MX setzen, SPF schreiben, DKIM eintragen, DMARC hinterher, Autoconfig und Co. nicht vergessen.</p>



<p>Weil ich darauf keine Lust mehr hatte, erledigt das bei mir jetzt ein Bash-Script, das:</p>



<ul class="wp-block-list">
<li>die DNS-Zone bei Cloudflare findet</li>



<li>den DKIM-Key automatisch aus der Mailcow-API zieht</li>



<li>alle relevanten DNS-Records für Mailcow setzt bzw. aktualisiert</li>
</ul>



<p></p>



<p>In diesem Beitrag zeige ich dir:</p>



<ul class="wp-block-list">
<li>welche DNS-Records angelegt werden und warum</li>



<li>wie das Script aufgebaut ist</li>



<li>wie du die <code>.env</code>-Datei für deine Secrets nutzt</li>



<li>wie du das Ganze in der Praxis einsetzt</li>
</ul>



<p>Am Ende findest du das komplette Script und die passende <code>.env</code>-Datei als kopierbare Code-Blöcke.</p>



<h2 class="wp-block-heading">Was das Script macht – einmal in Kurzform</h2>



<p>Für eine Domain wie <code>example.org</code> macht das Script:</p>



<ul class="wp-block-list">
<li><strong>MX</strong>: zeigt auf deinen Mailcow-Host (z. B. <code>mail.example.org</code>)</li>



<li><strong>SPF (TXT)</strong>: Standardmäßig <code>v=spf1 mx -all</code></li>



<li><strong>DKIM (TXT)</strong>: Holt den Key aus der Mailcow-API und legt <code>dkim._domainkey.example.org</code> (oder deinen Selector) an</li>



<li><strong>DMARC (TXT)</strong>: einfacher Default <code>v=DMARC1; p=quarantine; rua=mailto:dmarc-reports@example.org</code></li>



<li><strong>Autoconfig/Autodiscover (CNAME)</strong>: <code>autoconfig.example.org</code> und <code>autodiscover.example.org</code> → <code>mail.example.org</code></li>



<li><strong>MTA-STS (CNAME + TXT)</strong>: <code>mta-sts.example.org</code> + <code>_mta-sts.example.org</code></li>



<li><strong>TLS-Reporting (TXT)</strong>: <code>_smtp._tls.example.org</code> für TLS-Reports</li>
</ul>



<p>Wichtig: Existierende DKIM-Records mit anderen Selektoren (z. B. vom alten Mailserver) bleiben absichtlich unberührt. Solange du noch über den alten Server versendest oder alte Mails nachsigniert werden, ist das hilfreich. Aufräumen kannst du später manuell, wenn der alte Server wirklich komplett raus ist.</p>



<h2 class="wp-block-heading">Voraussetzungen</h2>



<p>Damit das Script sauber läuft, brauchst du:</p>



<ul class="wp-block-list">
<li>macOS oder Linux mit:</li>



<li><code>bash</code></li>



<li><code>curl</code></li>



<li><code>jq</code></li>



<li><strong>Cloudflare-API-Token</strong> mit:
<ul class="wp-block-list">
<li>Zone → DNS → Edit</li>



<li>Zone → Zone → Read</li>
</ul>
</li>



<li><strong>Mailcow-API-Key</strong> mit Leserechten für DKIM (<code>/api/v1/get/dkim/{domain}</code>)</li>



<li>Eine <strong>Mailcow-Instanz</strong>, z. B. <code>https://mailcow.example.org</code></li>



<li>Deine Domains müssen bereits als Zonen bei Cloudflare existieren</li>
</ul>



<h2 class="wp-block-heading">Wie das Script funktioniert</h2>



<h3 class="wp-block-heading">1. <code>.env</code>-Datei laden</h3>



<p>Das Script liest – falls vorhanden – eine <code>.env</code> im selben Verzeichnis.<br>Darin stehen die sensitiven Daten wie API-Tokens und URLs. So bleiben die Werte getrennt vom Script selbst.</p>



<pre class="wp-block-code"><code lang="bash" class="language-bash line-numbers"># .env automatisch laden, falls vorhanden
if [ -f .env ]; then
  echo "Lade .env..."
  set -o allexport
  source .env
  set +o allexport
fi</code></pre>



<p>Die Kombination aus <code>set -o allexport</code> und <code>source .env</code> sorgt dafür, dass jede Variable aus der <code>.env</code> automatisch als Umgebungsvariable exportiert wird, ohne dass du überall <code>export</code> schreiben musst.</p>



<h3 class="wp-block-heading">2. API-Variablen &amp; Standardwerte</h3>



<p>Das Script nutzt:</p>



<ul class="wp-block-list">
<li>Cloudflare API Token (<code>CF_API_TOKEN</code>)</li>



<li>Mailcow API Key (<code>MAILCOW_API_KEY</code>)</li>



<li>Mailcow Base-URL (<code>MAILCOW_BASEURL</code>)</li>



<li>Mail-Host / MX-Ziel (<code>MAIL_HOST</code>)</li>
</ul>



<p>Dazu kommen ein paar Default-Policies:</p>



<pre class="wp-block-code"><code lang="bash" class="language-bash line-numbers">CF_API_TOKEN="${CF_API_TOKEN:-}"
CF_API_BASE="https://api.cloudflare.com/client/v4"

MAILCOW_BASEURL="${MAILCOW_BASEURL:-https://mail.net73.de}"
MAILCOW_API_KEY="${MAILCOW_API_KEY:-}"

DOMAIN="${1:-}"                      # Domain als CLI-Argument
MAIL_HOST="${MAIL_HOST:-mail.net73.de}"

SPF_VALUE="${SPF_VALUE:-v=spf1 mx -all}"
DMARC_VALUE="${DMARC_VALUE:-v=DMARC1; p=quarantine; rua=mailto:dmarc-reports@${DOMAIN}}"
MTA_STS_ID="${MTA_STS_ID:-$(date +%Y%m%d)}"

TTL=3600</code></pre>



<p>Ein paar Punkte dazu:</p>



<ul class="wp-block-list">
<li><code>DOMAIN</code> kommt als erstes Argument, z. B. <code>./setup_mail_dns.sh example.org</code></li>



<li><code>SPF_VALUE</code>, <code>DMARC_VALUE</code>, <code>MTA_STS_ID</code> kannst du bei Bedarf über die <code>.env</code> überschreiben</li>



<li><code>MTA_STS_ID</code> nutzt einfach das aktuelle Datum als ID – reicht völlig</li>
</ul>



<h3 class="wp-block-heading">3. Fehler-Handling &amp; Pflicht-Variablen</h3>



<p>Damit du nicht mit halbgaren Umgebungen arbeitest, prüft das Script die nötigsten Variablen:</p>



<pre class="wp-block-code"><code lang="bash" class="language-bash line-numbers">err() { echo "ERROR: $*" &gt;&amp;2; exit 1; }

require_env() {
  local name="$1"
  [ -n "${!name:-}" ] || err "Umgebungsvariable $name ist nicht gesetzt."
}

[ -n "$DOMAIN" ] || err "Usage: $0 &lt;domain.tld&gt; (z.B. example.org)"

require_env CF_API_TOKEN
require_env MAILCOW_API_KEY
require_env MAILCOW_BASEURL</code></pre>



<p>Wenn z. B. <code>CF_API_TOKEN</code> fehlt, bricht das Script mit einer sauberen Fehlermeldung ab.</p>



<h3 class="wp-block-heading">4. Cloudflare-Zone finden</h3>



<p>Cloudflare arbeitet intern mit <strong>Zone-IDs</strong>. Die holt sich das Script über die API:</p>



<pre class="wp-block-code"><code lang="bash" class="language-bash line-numbers">echo "Suche Cloudflare Zone-ID für ${DOMAIN}..."
ZONE_ID=$(
  curl -s -X GET "${CF_API_BASE}/zones?name=${DOMAIN}" \
       -H "Authorization: Bearer ${CF_API_TOKEN}" \
       -H "Content-Type: application/json" \
    | jq -r '.result[0].id // empty'
)

[ -n "$ZONE_ID" ] || err "Konnte Zone-ID für ${DOMAIN} nicht finden. Stimmt die Domain in Cloudflare?"</code></pre>



<p>Damit bist du unabhängig von irgendwelchen manuellen IDs.</p>



<h3 class="wp-block-heading">5. DKIM-Key aus der Mailcow-API ziehen</h3>



<p>Statt den DKIM-Key per Copy &amp; Paste ins DNS zu kippen, zieht das Script den aktuellen Key direkt aus Mailcow:</p>



<pre class="wp-block-code"><code lang="bash" class="language-bash line-numbers">echo "Hole DKIM-Key von Mailcow für ${DOMAIN}..."

DKIM_JSON=$(
  curl -s -X GET "${MAILCOW_BASEURL}/api/v1/get/dkim/${DOMAIN}" \
       -H "X-API-Key: ${MAILCOW_API_KEY}"
)

DKIM_SELECTOR=$(echo "$DKIM_JSON" | jq -r '.dkim_selector // "dkim"')
DKIM_TXT=$(echo "$DKIM_JSON" | jq -r '.dkim_txt // empty')

[ -n "$DKIM_TXT" ] || err "Kein DKIM-TXT von Mailcow erhalten. Prüfe API-Key, Domain oder ob ein DKIM-Key angelegt wurde."

DKIM_NAME="${DKIM_SELECTOR}._domainkey.${DOMAIN}"

echo "DKIM-Selector: ${DKIM_SELECTOR}"
echo "DKIM-Record:   ${DKIM_NAME}"</code></pre>



<ul class="wp-block-list">
<li><code>dkim_selector</code>: z. B. <code>dkim</code></li>



<li><code>dkim_txt</code>: der komplette DKIM-Record, wie er ins DNS gehört (inkl. <code>v=DKIM1; k=rsa; p=…</code>)</li>
</ul>



<p>Das Script macht daraus später einen TXT-Record bei Cloudflare.</p>



<h3 class="wp-block-heading">6. DNS-Record-Handling mit <code>cf_upsert_record</code></h3>



<p>Der eigentliche Trick steckt in einer kleinen Hilfsfunktion, die Records entweder <strong>aktualisiert</strong> oder <strong>neu anlegt</strong>:</p>



<pre class="wp-block-code"><code lang="bash" class="language-bash line-numbers">cf_upsert_record() {
  local TYPE="$1"   # A, CNAME, MX, TXT, ...
  local NAME="$2"
  local CONTENT="$3"
  local TTL_VAL="$4"
  local PROXIED="${5:-false}"   # true/false (nur für A/AAAA/CNAME)
  local PRIORITY="${6:-0}"      # nur für MX relevant

  local DATA
  if [[ "$TYPE" == "MX" ]]; then
    DATA=$(jq -n --arg type "$TYPE" --arg name "$NAME" --arg content "$CONTENT" \
               --argjson ttl "$TTL_VAL" --argjson priority "$PRIORITY" \
               '{type:$type,name:$name,content:$content,ttl:$ttl,priority:$priority}')
  elif [[ "$TYPE" == "TXT" ]]; then
    DATA=$(jq -n --arg type "$TYPE" --arg name "$NAME" --arg content "$CONTENT" \
               --argjson ttl "$TTL_VAL" \
               '{type:$type,name:$name,content:$content,ttl:$ttl}')
  else
    DATA=$(jq -n --arg type "$TYPE" --arg name "$NAME" --arg content "$CONTENT" \
               --argjson ttl "$TTL_VAL" --argjson proxied "$PROXIED" \
               '{type:$type,name:$name,content:$content,ttl:$ttl,proxied:$proxied}')
  fi

  local EXISTING_ID
  EXISTING_ID=$(
    curl -s -X GET "${CF_API_BASE}/zones/${ZONE_ID}/dns_records?type=${TYPE}&amp;name=${NAME}" \
         -H "Authorization: Bearer ${CF_API_TOKEN}" \
         -H "Content-Type: application/json" \
      | jq -r '.result[0].id // empty'
  )

  if [[ -n "$EXISTING_ID" ]]; then
    curl -s -X PUT "${CF_API_BASE}/zones/${ZONE_ID}/dns_records/${EXISTING_ID}" \
         -H "Authorization: Bearer ${CF_API_TOKEN}" \
         -H "Content-Type: application/json" \
         --data "${DATA}" &gt;/dev/null
    echo "Updated ${TYPE} ${NAME}"
  else
    curl -s -X POST "${CF_API_BASE}/zones/${ZONE_ID}/dns_records" \
         -H "Authorization: Bearer ${CF_API_TOKEN}" \
         -H "Content-Type: application/json" \
         --data "${DATA}" &gt;/dev/null
    echo "Created ${TYPE} ${NAME}"
  fi
}</code></pre>



<p>Damit musst du dich im Hauptteil des Scripts nicht mehr darum kümmern, ob ein Record schon existiert.</p>



<h3 class="wp-block-heading">7. Welche DNS-Records das Script setzt</h3>



<p>Im letzten Block legt das Script alle relevanten Records für die Domain an bzw. aktualisiert sie:</p>



<pre class="wp-block-code"><code lang="bash" class="language-bash line-numbers">echo "Lege Standard-Mailcow-DNS-Einträge für ${DOMAIN} an / aktualisiere sie..."

# MX
cf_upsert_record "MX" "${DOMAIN}" "${MAIL_HOST}" "${TTL}" "false" 10

# SPF
cf_upsert_record "TXT" "${DOMAIN}" "${SPF_VALUE}" "${TTL}"

# DKIM
cf_upsert_record "TXT" "${DKIM_NAME}" "${DKIM_TXT}" "${TTL}"

# DMARC
cf_upsert_record "TXT" "_dmarc.${DOMAIN}" "${DMARC_VALUE}" "${TTL}"

# Autoconfig / Autodiscover
cf_upsert_record "CNAME" "autoconfig.${DOMAIN}" "${MAIL_HOST}" "${TTL}" "false"
cf_upsert_record "CNAME" "autodiscover.${DOMAIN}" "${MAIL_HOST}" "${TTL}" "false"

# Optional: MTA-STS &amp; TLS-RPT
cf_upsert_record "CNAME" "mta-sts.${DOMAIN}" "${MAIL_HOST}" "${TTL}" "false"
cf_upsert_record "TXT" "_mta-sts.${DOMAIN}" "v=STSv1; id=${MTA_STS_ID}" "${TTL}"
cf_upsert_record "TXT" "_smtp._tls.${DOMAIN}" "v=TLSRPTv1; rua=mailto:tls-reports@${DOMAIN}" "${TTL}"

echo "Fertig. Bitte DNS-Änderungen und Mailcow-Check-Tools (z.B. Mailcow DNS-Check, mail-tester.com) prüfen."</code></pre>



<p>Kurz dazu:</p>



<ul class="wp-block-list">
<li><strong>MX</strong>: zeigt auf deinen Mailcow-Host, Proxy ist ausgeschaltet (Cloudflare darf kein SMTP sprechen)</li>



<li><strong>SPF</strong>: minimalistisch, erlaubt nur den MX-Server</li>



<li><strong>DKIM</strong>: nutzt den aktuellen Key aus Mailcow</li>



<li><strong>DMARC</strong>: schickt Reports an <code>dmarc-reports@deine-domain</code></li>



<li><strong>Autoconfig/Autodiscover</strong>: hilft Mail-Clients bei der Auto-Konfiguration</li>



<li><strong>MTA-STS &amp; TLSRPT</strong>: härten Transportverschlüsselung und sammeln TLS-Reports</li>
</ul>



<h3 class="wp-block-heading">8. Was ist mit alten DKIM-Records?</h3>



<p>Viele migrieren von einem alten Mailserver (anderer Provider, eigene Kiste, …) zu Mailcow. Oft existieren dort bereits DKIM-Records mit einem anderen Selector, z. B.:</p>



<ul class="wp-block-list">
<li><code>google._domainkey.example.org</code></li>



<li><code>mailjet._domainkey.example.org</code></li>



<li><code>oldserver1._domainkey.example.org</code></li>
</ul>



<p>Das Script fasst diese <strong>bewusst nicht an</strong>.</p>



<p>Das ist kein Bug, sondern Feature: Solange der alte Mailserver noch Mails mit seinem Key signiert (oder alte Nachrichten Relevanz haben), ist es sinnvoll, die alten DKIM-Keys im DNS zu lassen. Erst wenn du sicher bist, dass der alte Server endgültig außer Betrieb ist, kannst du seine Selektoren gezielt löschen – das ist eine Entscheidung, die ich lieber manuell treffen möchte.</p>



<h2 class="wp-block-heading">Praxis: Setup, .env und Aufruf</h2>



<h3 class="wp-block-heading">1. Script speichern</h3>



<p>Speichere das Script z. B. als <code>setup_mail_dns.sh</code> in ein Verzeichnis deiner Wahl.</p>



<h3 class="wp-block-heading">2. <code>.env</code> anlegen</h3>



<p>Im selben Verzeichnis legst du eine Datei <code>.env</code> an, z. B.:</p>



<pre class="wp-block-code"><code lang="bash" class="language-bash line-numbers">export CF_API_TOKEN="xxxxxxx"
export MAILCOW_API_KEY="xxxxxxx"
export MAILCOW_BASEURL="https://mailcow.example.org"
export MAIL_HOST="mail.example.org"</code></pre>



<p>Für weitere Domains kannst du <code>MAIL_HOST</code> gleich lassen, wenn alle denselben Mailcow-Host nutzen.</p>



<h3 class="wp-block-heading">3. Script ausführbar machen und aufrufen</h3>



<pre class="wp-block-code"><code lang="bash" class="language-bash line-numbers">chmod +x setup_mail_dns.sh
./setup_mail_dns.sh example.org</code></pre>



<p>Für jede weitere Domain:</p>



<pre class="wp-block-code"><code lang="bash" class="language-bash line-numbers">./setup_mail_dns.sh andere-domain.tld</code></pre>



<h2 class="wp-block-heading">Komplettes Script (kopierfertig)</h2>



<pre class="wp-block-code"><code lang="bash" class="language-bash line-numbers">#!/usr/bin/env bash
# .env automatisch laden, falls vorhanden
if [ -f .env ]; then
  echo "Lade .env..."
  set -o allexport
  source .env
  set +o allexport
fi

set -euo pipefail

# 1) Cloudflare
CF_API_TOKEN="${CF_API_TOKEN:-}"    # besser als Umgebungsvariable setzen
CF_API_BASE="https://api.cloudflare.com/client/v4"

# 2) Mailcow
MAILCOW_BASEURL="${MAILCOW_BASEURL:-https://mail.net73.de}"  # ohne trailing slash
MAILCOW_API_KEY="${MAILCOW_API_KEY:-}"

# 3) Domain &amp; Mailserver
DOMAIN="${1:-}"           # z.B. example.org (als Argument)
MAIL_HOST="${MAIL_HOST:-mail.net73.de}"  # MX-Ziel

# 4) Mail-Policy Defaults
SPF_VALUE="${SPF_VALUE:-v=spf1 mx -all}"
DMARC_VALUE="${DMARC_VALUE:-v=DMARC1; p=quarantine; rua=mailto:dmarc-reports@${DOMAIN}}"
MTA_STS_ID="${MTA_STS_ID:-$(date +%Y%m%d)}"  # z.B. 20251115

TTL=3600

# ============================
# HILFSFUNKTIONEN
# ============================

err() { echo "ERROR: $*" &gt;&amp;2; exit 1; }

require_env() {
  local name="$1"
  [ -n "${!name:-}" ] || err "Umgebungsvariable $name ist nicht gesetzt."
}

# Upsert-Funktion für DNS-Records
cf_upsert_record() {
  local TYPE="$1"   # A, CNAME, MX, TXT, ...
  local NAME="$2"
  local CONTENT="$3"
  local TTL_VAL="$4"
  local PROXIED="${5:-false}"   # true/false (nur für A/AAAA/CNAME)
  local PRIORITY="${6:-0}"      # nur für MX relevant

  # JSON-Body je nach Typ bauen
  local DATA
  if [[ "$TYPE" == "MX" ]]; then
    DATA=$(jq -n --arg type "$TYPE" --arg name "$NAME" --arg content "$CONTENT" \
               --argjson ttl "$TTL_VAL" --argjson priority "$PRIORITY" \
               '{type:$type,name:$name,content:$content,ttl:$ttl,priority:$priority}')
  elif [[ "$TYPE" == "TXT" ]]; then
    DATA=$(jq -n --arg type "$TYPE" --arg name "$NAME" --arg content "$CONTENT" \
               --argjson ttl "$TTL_VAL" \
               '{type:$type,name:$name,content:$content,ttl:$ttl}')
  else
    # A/AAAA/CNAME mit proxied
    DATA=$(jq -n --arg type "$TYPE" --arg name "$NAME" --arg content "$CONTENT" \
               --argjson ttl "$TTL_VAL" --argjson proxied "$PROXIED" \
               '{type:$type,name:$name,content:$content,ttl:$ttl,proxied:$proxied}')
  fi

  # Existing Record abfragen
  local EXISTING_ID
  EXISTING_ID=$(
    curl -s -X GET "${CF_API_BASE}/zones/${ZONE_ID}/dns_records?type=${TYPE}&amp;name=${NAME}" \
         -H "Authorization: Bearer ${CF_API_TOKEN}" \
         -H "Content-Type: application/json" \
      | jq -r '.result[0].id // empty'
  )

  if [[ -n "$EXISTING_ID" ]]; then
    curl -s -X PUT "${CF_API_BASE}/zones/${ZONE_ID}/dns_records/${EXISTING_ID}" \
         -H "Authorization: Bearer ${CF_API_TOKEN}" \
         -H "Content-Type: application/json" \
         --data "${DATA}" &gt;/dev/null
    echo "Updated ${TYPE} ${NAME}"
  else
    curl -s -X POST "${CF_API_BASE}/zones/${ZONE_ID}/dns_records" \
         -H "Authorization: Bearer ${CF_API_TOKEN}" \
         -H "Content-Type: application/json" \
         --data "${DATA}" &gt;/dev/null
    echo "Created ${TYPE} ${NAME}"
  fi
}

# ============================
# CHECKS
# ============================

[ -n "$DOMAIN" ] || err "Usage: $0 &lt;domain.tld&gt; (z.B. example.org)"

require_env CF_API_TOKEN
require_env MAILCOW_API_KEY
require_env MAILCOW_BASEURL

# ============================
# ZONE ID HOLEN
# ============================

echo "Suche Cloudflare Zone-ID für ${DOMAIN}..."
ZONE_ID=$(
  curl -s -X GET "${CF_API_BASE}/zones?name=${DOMAIN}" \
       -H "Authorization: Bearer ${CF_API_TOKEN}" \
       -H "Content-Type: application/json" \
    | jq -r '.result[0].id // empty'
)

[ -n "$ZONE_ID" ] || err "Konnte Zone-ID für ${DOMAIN} nicht finden. Stimmt die Domain in Cloudflare?"

echo "Gefundene Zone-ID: ${ZONE_ID}"

# ============================
# DKIM PER MAILCOW-API HOLEN
# ============================

echo "Hole DKIM-Key von Mailcow für ${DOMAIN}..."

DKIM_JSON=$(
  curl -s -X GET "${MAILCOW_BASEURL}/api/v1/get/dkim/${DOMAIN}" \
       -H "X-API-Key: ${MAILCOW_API_KEY}"
)

DKIM_SELECTOR=$(echo "$DKIM_JSON" | jq -r '.dkim_selector // "dkim"')
DKIM_TXT=$(echo "$DKIM_JSON" | jq -r '.dkim_txt // empty')

[ -n "$DKIM_TXT" ] || err "Kein DKIM-TXT von Mailcow erhalten. Prüfe API-Key, Domain oder ob ein DKIM-Key angelegt wurde."

DKIM_NAME="${DKIM_SELECTOR}._domainkey.${DOMAIN}"

echo "DKIM-Selector: ${DKIM_SELECTOR}"
echo "DKIM-Record:   ${DKIM_NAME}"

# ============================
# DNS-RECORDS ANLEGEN / AKTUALISIEREN
# ============================

echo "Lege Standard-Mailcow-DNS-Einträge für ${DOMAIN} an / aktualisiere sie..."

# MX
cf_upsert_record "MX" "${DOMAIN}" "${MAIL_HOST}" "${TTL}" "false" 10

# SPF
cf_upsert_record "TXT" "${DOMAIN}" "${SPF_VALUE}" "${TTL}"

# DKIM
cf_upsert_record "TXT" "${DKIM_NAME}" "${DKIM_TXT}" "${TTL}"

# DMARC
cf_upsert_record "TXT" "_dmarc.${DOMAIN}" "${DMARC_VALUE}" "${TTL}"

# Autoconfig / Autodiscover
cf_upsert_record "CNAME" "autoconfig.${DOMAIN}" "${MAIL_HOST}" "${TTL}" "false"
cf_upsert_record "CNAME" "autodiscover.${DOMAIN}" "${MAIL_HOST}" "${TTL}" "false"

# Optional: MTA-STS &amp; TLS-RPT
cf_upsert_record "CNAME" "mta-sts.${DOMAIN}" "${MAIL_HOST}" "${TTL}" "false"
cf_upsert_record "TXT" "_mta-sts.${DOMAIN}" "v=STSv1; id=${MTA_STS_ID}" "${TTL}"
cf_upsert_record "TXT" "_smtp._tls.${DOMAIN}" "v=TLSRPTv1; rua=mailto:tls-reports@${DOMAIN}" "${TTL}"

echo "Fertig. Bitte DNS-Änderungen und Mailcow-Check-Tools (z.B. Mailcow DNS-Check, mail-tester.com) prüfen."</code></pre>



<h2 class="wp-block-heading">Beispiel-.env zum direkten Einsatz</h2>



<pre class="wp-block-code"><code lang="bash" class="language-bash line-numbers">export CF_API_TOKEN="xxxxxxx"
export MAILCOW_API_KEY="xxxxxxx"
export MAILCOW_BASEURL="https://mailcow.example.org"
export MAIL_HOST="mail.example.org"</code></pre><p>The post <a href="https://net73.de/automatisches-dns-setup-mit-cloudflare-und-mailcow-api/">Automatisches DNS-Setup mit Cloudflare- und Mailcow-API</a> first appeared on <a href="https://net73.de">NET73</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://net73.de/automatisches-dns-setup-mit-cloudflare-und-mailcow-api/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>CSS und JavaScript mit NPM und Gulp organisieren</title>
		<link>https://net73.de/css-und-javascript-mit-npm-und-gulp-organisieren/?utm_source=rss&#038;utm_medium=rss&#038;utm_campaign=css-und-javascript-mit-npm-und-gulp-organisieren</link>
					<comments>https://net73.de/css-und-javascript-mit-npm-und-gulp-organisieren/#respond</comments>
		
		<dc:creator><![CDATA[Marc Eggert]]></dc:creator>
		<pubDate>Wed, 11 Mar 2020 19:07:20 +0000</pubDate>
				<category><![CDATA[Linux]]></category>
		<category><![CDATA[WebDev]]></category>
		<category><![CDATA[development]]></category>
		<category><![CDATA[gulp]]></category>
		<category><![CDATA[nodejs]]></category>
		<category><![CDATA[npm]]></category>
		<category><![CDATA[php]]></category>
		<category><![CDATA[yarn]]></category>
		<guid isPermaLink="false">https://pixel64.de/?p=1933</guid>

					<description><![CDATA[<p>Bei den meisten Web-Projekten setze ich auf verschiedene externe Projekte und Frameworks. Sei es Bootstrap, JQuery oder FontAwesome und weitere Klassen oder Plugins. Meistens ist es dann ein Problem, die verschiedenen Versionen und externen Scripts auf einem aktuellen Stand zu halten, zu organisieren oder kontrollieren, welche Version untereinander notwendig ist, um alle Abhängigkeiten (auch die [&#8230;]</p>
<p>The post <a href="https://net73.de/css-und-javascript-mit-npm-und-gulp-organisieren/">CSS und JavaScript mit NPM und Gulp organisieren</a> first appeared on <a href="https://net73.de">NET73</a>.</p>]]></description>
										<content:encoded><![CDATA[<p class="has-drop-cap has-very-light-gray-background-color has-background">Bei den meisten Web-Projekten setze ich auf verschiedene externe Projekte und Frameworks. Sei es Bootstrap, JQuery oder FontAwesome und weitere Klassen oder Plugins. Meistens ist es dann ein Problem, die verschiedenen Versionen und externen Scripts auf einem aktuellen Stand zu halten, zu organisieren oder kontrollieren, welche Version untereinander notwendig ist, um alle Abhängigkeiten (auch die des eigenen Projekts) zu erfüllen.<br>Um die jeweils aktuellen oder selbst gesetzten festen Versionen von Bootstrap &amp; Co. zu halten, gibt es Nodejs/NPM. Sämtliche Pakete werden dabei in ein Verzeichnis &#8222;node_modules&#8220; installiert. Doch wie sieht eine praktikable Lösung aus, mittels welcher diese Bibliotheken im eigenen Web-Projekt gehalten werden?<br>Genau hier kommt der &#8222;Workflow-Enhancer&#8220; Gulp.js ins Spiel. Gulp sorgt dafür, dass die entscheidenden Scripts aus dem Verzeichnis node_modules in das eigene Web-Verzeichnis geführt werden (beispielsweise /js, /css, /fonts, etc.) und ggf. SASS&#8211;Code aus Bootstrap und eigenem Code in CSS kompiliert wird. Dazu ist in der DEV-Umgebung ein File-Watcher aktiv, welcher in Echtzeit geschriebenen SASS-Code kompiliert und ins CSS-Verzeichnis schreibt.<br>Im Anschluss (nicht Teil dieses Beitrags) kann über eine Aufgabe via Gulp der gesamte JS- und CSS-Code für die Produktiv-Umgebung komprimiert werden. Perfekt!</p>



<span id="more-1933"></span>



<h2 class="wp-block-heading">Ausgangslage (Beispiel)</h2>



<p>Grundvoraussetzung für den nachfolgend beschriebenen Einsatz von gulp ist, dass bereits <a rel="noreferrer noopener" aria-label="NodeJS (öffnet in neuem Tab)" href="https://nodejs.org/" target="_blank">NodeJS</a> und npm (oder <a rel="noreferrer noopener" aria-label="yarn (öffnet in neuem Tab)" href="https://yarnpkg.com/" target="_blank">yarn</a>) in der Entwicklungsumgebung installiert sind.</p>



<p>Ein PHP-Web-Projekt sieht bei mir meistens so aus, dass ich im obersten Verzeichnis alle Umgebungs-Tools ablege (Dateien für Docker, Vagrant, etc.). Dort ist dann auch ein Verzeichnis &#8222;www&#8220;, in welchem das eigentliche Projekt liegt. In diesem Verzeichnis sind in der Wurzel dann die Dateien für die DEV-Tools (Composer, Node, Gulp, etc.), ein &#8222;app&#8220;-Verzeichnis, in welchem die PHP-Aktionen sind (MVC) und ein &#8222;public&#8220;-Verzeichnis, welches die &#8222;htdocs&#8220; für die Webroot bereithält. Hier ist meistens nur eine index.php-Datei, welche die Core-Klasse(autoload) aus dem app-Verzeichnis lädt und eben die statischen Frontend-Dateien (js,css, scss, assets, fonts, etc.).</p>



<p>In der Übersicht sieht das dann so aus:</p>



<pre class="wp-block-code"><code lang="bash" class="language-bash">├── app -&gt;
│   ├── init.php
│   ├── config -&gt;
│   ├── controller -&gt;
│   ├── library -&gt;
│   ├── model -&gt;
│   └── view -&gt;
├── composer.json
├── composer.lock
├── composer.phar
├── gulpfile.js
(├── node_modules -&gt; # wird erst mit "npm install" automatisch angelegt!)
├── package.json
├── package-lock.json
├── public -&gt;
│   ├── css -&gt;
│   ├── js -&gt;
│   ├── fonts -&gt;
│   ├── scss -&gt;
│   └── index.php
└── Readme.md</code></pre>



<p>Das Ziel ist hier also, dass die benötigten JS- und CSS-Dateien aus node_modules in das Frontend-Verzeichnis nach public/js und public/css überführt werden und die selbst geschriebenen SASS-Scripts aus public/scss mit den Scripts aus node_modules zusammen in public/css kompiliert werden (letzteres inkl. FileWatcher in Echtzeit.</p>



<p>Als Basis-Beispiel eine package.json-Datei, die bereits Gulp lokal via npm (oder yarn) installiert:</p>



<pre class="wp-block-code"><code lang="json" class="language-json line-numbers">{
  "name": "n73-test",
  "version": "0.1.0",
  "description": "Test-Projekt zur gulp-Demonstration",
  "main": "public/index.php",
  "scripts": {
    "test": "echo \"Error: no test specified\" &amp;&amp; exit 1"
  },
  "author": "Pixel64",
  "license": "MIT",
  "dependencies": {
    "bootstrap": "^4.4.1",
    "jquery": "^3.4.1",
    "popper.js": "^1.16.1"
  },
  "devDependencies": {
    "browser-sync": "^2.26.7",
    "gulp": "^4.0.2",
    "gulp-sass": "^4.0.2"
  }
}</code></pre>



<p>In diesem Projekt werden also die Pakete Bootstrap, JQuery und Popper gezogen und in das Verzeichnis node_modules gelegt.</p>



<h2 class="wp-block-heading">GULP.JS</h2>



<p>Mit der obigen Verzeichnisstruktur werden die Module via &#8222;npm install&#8220; installiert (im Root-Verzeichnis, in welchem sich auch die package.json-Datei befindet).</p>



<p>Ist der Vorgang ausgeführt ist gulp _lokal_ installiert! Eine globale Installation von gulp(-cli) kann über den Paketmanager oder mit dem Schalter &#8222;-g&#8220; erreicht werden (npm install -g gulp).</p>



<p>Die lokale Projekt-Installation reicht allerdings i.d.R. aus. Die installierte Version kann wie folgt geprüft werden:</p>



<pre class="wp-block-code"><code lang="bash" class="language-bash">./node_modules/.bin/gulp --version
CLI version: 2.2.0
Local version: 4.0.2</code></pre>



<h3 class="wp-block-heading">gulpfile.js</h3>



<p>Die Anweisungen, was gulp im Verzeichnis erledigen soll, werden in der Datei gulpfile.js festgehalten. Eine Beispieldatei für das Test-Projekt sieht so aus:</p>



<pre class="wp-block-code"><code lang="javascript" class="language-javascript line-numbers">var gulp        = require('gulp');
var browserSync = require('browser-sync').create();
var sass        = require('gulp-sass');

// SASS kompilieren, nach public/css verschieben und in browsersync aufnehmen
gulp.task('sass', function(){
    return gulp.src(['node_modules/bootstrap/scss/bootstrap.scss', 'public/scss/*.scss'])
        .pipe(sass())
        .pipe(gulp.dest('public/css'))
        .pipe(browserSync.stream());
});

//JS nach public/js verschieben
gulp.task('js', function(){
    return gulp.src([
        'node_modules/bootstrap/dist/js/bootstrap.min.js', 
        'node_modules/jquery/dist/jquery.min.js', 
        'node_modules/popper.js/dist/umd/popper.min.js'])
        .pipe(gulp.dest('public/js'))
        .pipe(browserSync.stream());
});



//Fonts nach public/fonts schieben
gulp.task('fonts', function(){
    return gulp.src('node_modules/font-awesome/fonts/*')
        .pipe(gulp.dest('public/fonts'));
});

//fontawesome nach public/css verschieben
gulp.task('fa', ()=&gt;{
    return gulp.src('node_modules/font-awesome/css/font-awesome.min.css')
        .pipe(gulp.dest('public/css'))
        .pipe(browserSync.stream());
});

// SASS ueberwachen und bei Veraenderungen kompilieren
gulp.task('serve', gulp.series('sass', function() {
    browserSync.init({
	    proxy: 'http://localhost/',
	    notify: true,
    });

    gulp.watch(['node_modules/bootstrap/scss/bootstrap.scss', 'scss/*.scss'], gulp.series('sass'))
    gulp.watch('*.php').on('change', browserSync.reload);
}));


gulp.task('default', gulp.series('js', 'serve', 'fa', 'fonts'));%</code></pre>



<p>Ist diese Datei erstellt, reicht ein einfacher Aufruf von ./node_modules/.bin/gulp und der Vorgang startet. Jetzt werden die entsprechenden Scripts kopiert und ein aktiver FileWatcher überwacht SASS und kompiliert bei Veränderungen scss nach css. Zudem werden die Veränderungen an den definierten Dateien im www-Verzeichnis über einen lokalen Server (Port 3000) angezeigt (index.php oder index.html, je nach Projekt). Letzteres wird durch den gulp-task &#8222;server&#8220; organisiert und kann bei Bedarf oder Nutzung anderer Entwicklungsumgebungen deaktiviert werden (Docker- oder Vagrant-Umgebung?).</p>



<h2 class="wp-block-heading">Zusatz</h2>



<p>Natürlich kann gulp auch in komplett statischen Umgebungen eingesetzt werden. Generell müssen jeweils nur die Verzeichnisse an die eigene Projektstruktur angepasst werden.</p>



<p>Über die package.json-Datei kann jeweils kontrolliert werden, welche Versionen von Bootstrap, JQuery &amp; Co. geladen werden sollen und den Weg ins eigene Web-Projekt finden. Die so erstellten CSS- und JS-Dateien können dann wie gewohnt aus dem /js- und /css-Verzeichnis in die eigenen Web-Dateien (PHP-&gt;Views, oder HTML-Dateien-&gt;statisch) eingebunden werden.</p>



<p>Für das Deployment im Produktiv-Betrieb ist somit auch kein einzelnes Zusammensuchen der Bibliotheken notwendig und auch kein Überwachen der statisch eingebundenen CDN-Links im Projekt notwendig (kann man trotzdem parallel nutzen).</p><p>The post <a href="https://net73.de/css-und-javascript-mit-npm-und-gulp-organisieren/">CSS und JavaScript mit NPM und Gulp organisieren</a> first appeared on <a href="https://net73.de">NET73</a>.</p>]]></content:encoded>
					
					<wfw:commentRss>https://net73.de/css-und-javascript-mit-npm-und-gulp-organisieren/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
