]> git.proxmox.com Git - mirror_acme.sh.git/commitdiff
Syncing with the original repo (#2)
authorAlexander Kulumbeg <kulumbeg@gmail.com>
Sat, 20 Mar 2021 15:01:09 +0000 (16:01 +0100)
committerGitHub <noreply@github.com>
Sat, 20 Mar 2021 15:01:09 +0000 (16:01 +0100)
* change arvan api script

* change Author name

* change name actor

* Updated --preferred-chain to issue ISRG properly

To support different openssl crl2pkcs7 help cli format

* dnsapi/pdns: also normalize json response in detecting root zone

* Chain (#3408)

* fix https://github.com/acmesh-official/acme.sh/issues/3384
match the issuer to the root CA cert subject

* fix format

* fix https://github.com/acmesh-official/acme.sh/issues/3384

* remove the alt files. https://github.com/acmesh-official/acme.sh/issues/3384

* upgrade freebsd and solaris

* duckdns - fix "integer expression expected" errors (#3397)

* fix "integer expression expected" errors

* duckdns fix

* Update dns_duckdns.sh

* Update dns_duckdns.sh

* Implement smtp notify hook

Support notifications via direct SMTP server connection.
Uses Python (2.7.x or 3.4+) to communicate with SMTP server.

* Make shfmt happy

(I'm open to better ways of formatting the heredoc
that embeds the Python script.)

* Only save config if send is successful

* Add instructions for reporting bugs

* Prep for curl or Python; clean up SMTP_* variable usage

* Implement curl version of smtp notify-hook

* More than one blank line is an abomination, apparently

I will not try to use whitespace to group code visually

* Fix: Unifi deploy hook support Unifi Cloud Key (#3327)

* fix: unifi deploy hook also update Cloud Key nginx certs

When running on a Unifi Cloud Key device, also deploy to
/etc/ssl/private/cloudkey.{crt,key} and reload nginx. This
makes the new cert available for the Cloud Key management
app running via nginx on port 443 (as well as the port 8443
Unifi Controller app the deploy hook already supported).

Fixes #3326

* Improve settings documentation comments

* Improve Cloud Key pre-flight error messaging

* Fix typo

* Add support for UnifiOS (Cloud Key Gen2)

Since UnifiOS does not use the Java keystore (like a Unifi
Controller or Cloud Key Gen1 deploy), this also reworks
the settings validation and error messaging somewhat.

* PR review fixes

* Detect unsupported Cloud Key java keystore location

* Don't try to restart inactive services

(and remove extra spaces from reload command)

* Clean up error messages and internal variables

* Change to _getdeployconf/_savedeployconf

* Switch from cp to cat to preserve file permissions

* feat: add huaweicloud error handling

* fix: fix freebsd and solaris

* support openssl 3.0
fix https://github.com/acmesh-official/acme.sh/issues/3399

* make the fix for rsa key only

* Use PROJECT_NAME and VER for X-Mailer header

Also add X-Mailer header to Python version

* Add _clearaccountconf_mutable()

* Rework read/save config to not save default values

Add and use _readaccountconf_mutable_default and
_saveaccountconf_mutable_default helpers to capture
common default value handling.

New approach also eliminates need for separate
underscore-prefixed version of each conf var.

* Implement _rfc2822_date helper

* Clean email headers and warn on unsupported address format

Just in case, make sure CR or NL don't end up in
an email header.

* Clarify _readaccountconf_mutable_default

* Add Date email header in Python implementation

* Use email.policy.default in Python 3 implementation

Improves standards compatibility and utf-8 handling
in Python 3.3-3.8. (email.policy.default becomes the
default in Python 3.9.)

* Prefer Python to curl when both available

* Change default SMTP_SECURE to "tls"

Secure by default. Also try to minimize configuration errors.
(Many ESPs/ISPs require STARTTLS, and most support it.)

* Update dns_dp.sh

没有encode中文字符会导致提交失败

* No need to include EC parameters explicitly with the private key.
(they are embedded)

* Fixes response handling and thereby allow issuing of subdomain certs

* Adds comment

* fix https://github.com/acmesh-official/acme.sh/issues/3402

* dnsapi/ionos: Use POST instead of PATCH for adding TXT record

The API now supports a POST route for adding records. Therefore
checking for already existing records and including them in a PATCH
request is no longer necessary.

* fix https://github.com/acmesh-official/acme.sh/issues/3433

* fix https://github.com/acmesh-official/acme.sh/issues/3019

* fix format

* Update dns_servercow.sh to support wildcard certs

Updated dns_servercow.sh to support txt records with multiple entries. This supports wildcard certificates that require txt records with the same name and different contents.

* Update dns_servercow.sh to support wildcard certs

Updated dns_servercow.sh to support txt records with multiple entries. This supports wildcard certificates that require txt records with the same name and different contents.

* fix https://github.com/acmesh-official/acme.sh/issues/3312

* fix format

* feat: add dns_porkbun

* fix: prevent rate limit

Co-authored-by: Vahid Fardi <vahid.fardi@snapp.cab>
Co-authored-by: neil <github@neilpang.com>
Co-authored-by: Gnought <1684105+gnought@users.noreply.github.com>
Co-authored-by: manuel <manuel@mausz.at>
Co-authored-by: jerrm <jerrm@users.noreply.github.com>
Co-authored-by: medmunds <medmunds@gmail.com>
Co-authored-by: Mike Edmunds <github@to.mikeedmunds.com>
Co-authored-by: Easton Man <manyang.me@outlook.com>
Co-authored-by: czeming <loser_wind@163.com>
Co-authored-by: Geert Hendrickx <geert@hendrickx.be>
Co-authored-by: Kristian Johansson <kristian.johansson86@gmail.com>
Co-authored-by: Lukas Brocke <lukas@brocke.net>
Co-authored-by: anom-human <80478363+anom-human@users.noreply.github.com>
Co-authored-by: neil <win10@neilpang.com>
Co-authored-by: Quentin Dreyer <quentin.dreyer@rgsystem.com>
15 files changed:
.github/workflows/DNS.yml
.github/workflows/LetsEncrypt.yml
acme.sh
deploy/unifi.sh
dnsapi/dns_arvan.sh
dnsapi/dns_dp.sh
dnsapi/dns_duckdns.sh
dnsapi/dns_huaweicloud.sh
dnsapi/dns_ionos.sh
dnsapi/dns_namecheap.sh
dnsapi/dns_pdns.sh
dnsapi/dns_porkbun.sh [new file with mode: 0644]
dnsapi/dns_servercow.sh
dnsapi/dns_simply.sh
notify/smtp.sh

index 5dc2d453469f62eb5e86616174c515168129b7f3..ed0426ad7e1543b383ee8384d5aaa8353861f47a 100644 (file)
@@ -184,7 +184,7 @@ jobs:
     - uses: actions/checkout@v2\r
     - name: Clone acmetest\r
       run: cd .. && git clone https://github.com/acmesh-official/acmetest.git  && cp -r acme.sh acmetest/\r
-    - uses: vmactions/freebsd-vm@v0.0.7\r
+    - uses: vmactions/freebsd-vm@v0.1.2\r
       with:\r
         envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}'\r
         prepare: pkg install -y socat curl\r
@@ -223,7 +223,7 @@ jobs:
     - uses: actions/checkout@v2\r
     - name: Clone acmetest\r
       run: cd .. && git clone https://github.com/acmesh-official/acmetest.git  && cp -r acme.sh acmetest/\r
-    - uses: vmactions/solaris-vm@v0.0.1\r
+    - uses: vmactions/solaris-vm@v0.0.3\r
       with:\r
         envs: 'TEST_DNS TestingDomain TEST_DNS_NO_WILDCARD TEST_DNS_SLEEP CASE TEST_LOCAL DEBUG ${{ secrets.TokenName1}} ${{ secrets.TokenName2}} ${{ secrets.TokenName3}} ${{ secrets.TokenName4}} ${{ secrets.TokenName5}}'\r
         prepare: pkgutil -y -i socat curl\r
index 8d0c4eb01f4ef0457431913e01adef0f6112895b..7c398c09dce023c08a475b0aaf32dea8c1b67078 100644 (file)
@@ -111,7 +111,7 @@ jobs:
     - uses: actions/checkout@v2\r
     - name: Clone acmetest\r
       run: cd .. && git clone https://github.com/acmesh-official/acmetest.git  && cp -r acme.sh acmetest/\r
-    - uses: vmactions/freebsd-vm@v0.0.7\r
+    - uses: vmactions/freebsd-vm@v0.1.2\r
       with:\r
         envs: 'NGROK_TOKEN TEST_LOCAL'\r
         prepare: pkg install -y socat curl\r
@@ -136,7 +136,7 @@ jobs:
       run: echo "TestingDomain=${{steps.ngrok.outputs.server}}" >> $GITHUB_ENV\r
     - name: Clone acmetest\r
       run: cd .. && git clone https://github.com/acmesh-official/acmetest.git  && cp -r acme.sh acmetest/\r
-    - uses: vmactions/solaris-vm@v0.0.1\r
+    - uses: vmactions/solaris-vm@v0.0.3\r
       with:\r
         envs: 'TEST_LOCAL TestingDomain'\r
         nat: |\r
diff --git a/acme.sh b/acme.sh
index a1ad41951e8249d33b479db503ca5f7ce1e6bf59..8d422719e3ee372fa3e79dfd432800803fac999e 100755 (executable)
--- a/acme.sh
+++ b/acme.sh
@@ -562,8 +562,16 @@ if _exists xargs && [ "$(printf %s '\\x41' | xargs printf)" = 'A' ]; then
 fi
 
 _h2b() {
-  if _exists xxd && xxd -r -p 2>/dev/null; then
-    return
+  if _exists xxd; then
+    if _contains "$(xxd --help 2>&1)" "assumes -c30"; then
+      if xxd -r -p -c 9999 2>/dev/null; then
+        return
+      fi
+    else
+      if xxd -r -p 2>/dev/null; then
+        return
+      fi
+    fi
   fi
 
   hex=$(cat)
@@ -1124,7 +1132,7 @@ _createkey() {
 
   if _isEccKey "$length"; then
     _debug "Using ec name: $eccname"
-    if _opkey="$(${ACME_OPENSSL_BIN:-openssl} ecparam -name "$eccname" -genkey 2>/dev/null)"; then
+    if _opkey="$(${ACME_OPENSSL_BIN:-openssl} ecparam -name "$eccname" -noout -genkey 2>/dev/null)"; then
       echo "$_opkey" >"$f"
     else
       _err "error ecc key name: $eccname"
@@ -1132,7 +1140,11 @@ _createkey() {
     fi
   else
     _debug "Using RSA: $length"
-    if _opkey="$(${ACME_OPENSSL_BIN:-openssl} genrsa "$length" 2>/dev/null)"; then
+    __traditional=""
+    if _contains "$(${ACME_OPENSSL_BIN:-openssl} help genrsa 2>&1)" "-traditional"; then
+      __traditional="-traditional"
+    fi
+    if _opkey="$(${ACME_OPENSSL_BIN:-openssl} genrsa $__traditional "$length" 2>/dev/null)"; then
       echo "$_opkey" >"$f"
     else
       _err "error rsa key: $length"
@@ -2121,6 +2133,12 @@ _send_signed_request() {
         _sleep $_sleep_retry_sec
         continue
       fi
+      if _contains "$_body" "The Replay Nonce is not recognized"; then
+        _info "The replay Nonce is not valid, let's get a new one, Sleeping $_sleep_retry_sec seconds."
+        _CACHED_NONCE=""
+        _sleep $_sleep_retry_sec
+        continue
+      fi
     fi
     return 0
   done
@@ -2279,6 +2297,13 @@ _clearaccountconf() {
   _clear_conf "$ACCOUNT_CONF_PATH" "$1"
 }
 
+#key
+_clearaccountconf_mutable() {
+  _clearaccountconf "SAVED_$1"
+  #remove later
+  _clearaccountconf "$1"
+}
+
 #_savecaconf  key  value
 _savecaconf() {
   _save_conf "$CA_CONF" "$1" "$2"
@@ -4009,12 +4034,42 @@ _check_dns_entries() {
 }
 
 #file
-_get_cert_issuers() {
+_get_chain_issuers() {
   _cfile="$1"
-  if _contains "$(${ACME_OPENSSL_BIN:-openssl} help crl2pkcs7 2>&1)" "Usage: crl2pkcs7" || _contains "$(${ACME_OPENSSL_BIN:-openssl} crl2pkcs7 help 2>&1)" "unknown option help"; then
-    ${ACME_OPENSSL_BIN:-openssl} crl2pkcs7 -nocrl -certfile $_cfile | ${ACME_OPENSSL_BIN:-openssl} pkcs7 -print_certs -text -noout | grep 'Issuer:' | _egrep_o "CN *=[^,]*" | cut -d = -f 2
+  if _contains "$(${ACME_OPENSSL_BIN:-openssl} help crl2pkcs7 2>&1)" "Usage: crl2pkcs7" || _contains "$(${ACME_OPENSSL_BIN:-openssl} crl2pkcs7 -help 2>&1)" "Usage: crl2pkcs7" || _contains "$(${ACME_OPENSSL_BIN:-openssl} crl2pkcs7 help 2>&1)" "unknown option help"; then
+    ${ACME_OPENSSL_BIN:-openssl} crl2pkcs7 -nocrl -certfile $_cfile | ${ACME_OPENSSL_BIN:-openssl} pkcs7 -print_certs -text -noout | grep -i 'Issuer:' | _egrep_o "CN *=[^,]*" | cut -d = -f 2
   else
-    ${ACME_OPENSSL_BIN:-openssl} x509 -in $_cfile -text -noout | grep 'Issuer:' | _egrep_o "CN *=[^,]*" | cut -d = -f 2
+    _cindex=1
+    for _startn in $(grep -n -- "$BEGIN_CERT" "$_cfile" | cut -d : -f 1); do
+      _endn="$(grep -n -- "$END_CERT" "$_cfile" | cut -d : -f 1 | _head_n $_cindex | _tail_n 1)"
+      _debug2 "_startn" "$_startn"
+      _debug2 "_endn" "$_endn"
+      if [ "$DEBUG" ]; then
+        _debug2 "cert$_cindex" "$(sed -n "$_startn,${_endn}p" "$_cfile")"
+      fi
+      sed -n "$_startn,${_endn}p" "$_cfile" | ${ACME_OPENSSL_BIN:-openssl} x509 -text -noout | grep 'Issuer:' | _egrep_o "CN *=[^,]*" | cut -d = -f 2 | sed "s/ *\(.*\)/\1/"
+      _cindex=$(_math $_cindex + 1)
+    done
+  fi
+}
+
+#
+_get_chain_subjects() {
+  _cfile="$1"
+  if _contains "$(${ACME_OPENSSL_BIN:-openssl} help crl2pkcs7 2>&1)" "Usage: crl2pkcs7" || _contains "$(${ACME_OPENSSL_BIN:-openssl} crl2pkcs7 -help 2>&1)" "Usage: crl2pkcs7" || _contains "$(${ACME_OPENSSL_BIN:-openssl} crl2pkcs7 help 2>&1)" "unknown option help"; then
+    ${ACME_OPENSSL_BIN:-openssl} crl2pkcs7 -nocrl -certfile $_cfile | ${ACME_OPENSSL_BIN:-openssl} pkcs7 -print_certs -text -noout | grep -i 'Subject:' | _egrep_o "CN *=[^,]*" | cut -d = -f 2
+  else
+    _cindex=1
+    for _startn in $(grep -n -- "$BEGIN_CERT" "$_cfile" | cut -d : -f 1); do
+      _endn="$(grep -n -- "$END_CERT" "$_cfile" | cut -d : -f 1 | _head_n $_cindex | _tail_n 1)"
+      _debug2 "_startn" "$_startn"
+      _debug2 "_endn" "$_endn"
+      if [ "$DEBUG" ]; then
+        _debug2 "cert$_cindex" "$(sed -n "$_startn,${_endn}p" "$_cfile")"
+      fi
+      sed -n "$_startn,${_endn}p" "$_cfile" | ${ACME_OPENSSL_BIN:-openssl} x509 -text -noout | grep -i 'Subject:' | _egrep_o "CN *=[^,]*" | cut -d = -f 2 | sed "s/ *\(.*\)/\1/"
+      _cindex=$(_math $_cindex + 1)
+    done
   fi
 }
 
@@ -4022,14 +4077,12 @@ _get_cert_issuers() {
 _match_issuer() {
   _cfile="$1"
   _missuer="$2"
-  _fissuers="$(_get_cert_issuers $_cfile)"
+  _fissuers="$(_get_chain_issuers $_cfile)"
   _debug2 _fissuers "$_fissuers"
-  if _contains "$_fissuers" "$_missuer"; then
-    return 0
-  fi
-  _fissuers="$(echo "$_fissuers" | _lower_case)"
+  _rootissuer="$(echo "$_fissuers" | _lower_case | _tail_n 1)"
+  _debug2 _rootissuer "$_rootissuer"
   _missuer="$(echo "$_missuer" | _lower_case)"
-  _contains "$_fissuers" "$_missuer"
+  _contains "$_rootissuer" "$_missuer"
 }
 
 #webroot, domain domainlist  keylength
@@ -4803,6 +4856,9 @@ $_authorizations_map"
     _split_cert_chain "$CERT_PATH" "$CERT_FULLCHAIN_PATH" "$CA_CERT_PATH"
 
     if [ "$_preferred_chain" ] && [ -f "$CERT_FULLCHAIN_PATH" ]; then
+      if [ "$DEBUG" ]; then
+        _debug "default chain issuers: " "$(_get_chain_issuers "$CERT_FULLCHAIN_PATH")"
+      fi
       if ! _match_issuer "$CERT_FULLCHAIN_PATH" "$_preferred_chain"; then
         rels="$(echo "$responseHeaders" | tr -d ' <>' | grep -i "^link:" | grep -i 'rel="alternate"' | cut -d : -f 2- | cut -d ';' -f 1)"
         _debug2 "rels" "$rels"
@@ -4818,13 +4874,22 @@ $_authorizations_map"
           _relca="$CA_CERT_PATH.alt"
           echo "$response" >"$_relcert"
           _split_cert_chain "$_relcert" "$_relfullchain" "$_relca"
+          if [ "$DEBUG" ]; then
+            _debug "rel chain issuers: " "$(_get_chain_issuers "$_relfullchain")"
+          fi
           if _match_issuer "$_relfullchain" "$_preferred_chain"; then
             _info "Matched issuer in: $rel"
             cat $_relcert >"$CERT_PATH"
             cat $_relfullchain >"$CERT_FULLCHAIN_PATH"
             cat $_relca >"$CA_CERT_PATH"
+            rm -f "$_relcert"
+            rm -f "$_relfullchain"
+            rm -f "$_relca"
             break
           fi
+          rm -f "$_relcert"
+          rm -f "$_relfullchain"
+          rm -f "$_relca"
         done
       fi
     fi
@@ -5222,6 +5287,7 @@ signcsr() {
   _renew_hook="${10}"
   _local_addr="${11}"
   _challenge_alias="${12}"
+  _preferred_chain="${13}"
 
   _csrsubj=$(_readSubjectFromCSR "$_csrfile")
   if [ "$?" != "0" ]; then
@@ -5268,7 +5334,7 @@ signcsr() {
   _info "Copy csr to: $CSR_PATH"
   cp "$_csrfile" "$CSR_PATH"
 
-  issue "$_csrW" "$_csrsubj" "$_csrdomainlist" "$_csrkeylength" "$_real_cert" "$_real_key" "$_real_ca" "$_reload_cmd" "$_real_fullchain" "$_pre_hook" "$_post_hook" "$_renew_hook" "$_local_addr" "$_challenge_alias"
+  issue "$_csrW" "$_csrsubj" "$_csrdomainlist" "$_csrkeylength" "$_real_cert" "$_real_key" "$_real_ca" "$_reload_cmd" "$_real_fullchain" "$_pre_hook" "$_post_hook" "$_renew_hook" "$_local_addr" "$_challenge_alias" "$_preferred_chain"
 
 }
 
@@ -7365,7 +7431,7 @@ _process() {
     deploy "$_domain" "$_deploy_hook" "$_ecc"
     ;;
   signcsr)
-    signcsr "$_csr" "$_webroot" "$_cert_file" "$_key_file" "$_ca_file" "$_reloadcmd" "$_fullchain_file" "$_pre_hook" "$_post_hook" "$_renew_hook" "$_local_address" "$_challenge_alias"
+    signcsr "$_csr" "$_webroot" "$_cert_file" "$_key_file" "$_ca_file" "$_reloadcmd" "$_fullchain_file" "$_pre_hook" "$_post_hook" "$_renew_hook" "$_local_address" "$_challenge_alias" "$_preferred_chain"
     ;;
   showcsr)
     showcsr "$_csr" "$_domain"
index 184aa62e8850eb079b3800250eab006979ffc8dd..a864135e82cd0895536d8514f47dcc6ad680425d 100644 (file)
@@ -1,12 +1,43 @@
 #!/usr/bin/env sh
 
-#Here is a script to deploy cert to unifi server.
+# Here is a script to deploy cert on a Unifi Controller or Cloud Key device.
+# It supports:
+#   - self-hosted Unifi Controller
+#   - Unifi Cloud Key (Gen1/2/2+)
+#   - Unifi Cloud Key running UnifiOS (v2.0.0+, Gen2/2+ only)
+# Please report bugs to https://github.com/acmesh-official/acme.sh/issues/3359
 
 #returns 0 means success, otherwise error.
 
+# The deploy-hook automatically detects standard Unifi installations
+# for each of the supported environments. Most users should not need
+# to set any of these variables, but if you are running a self-hosted
+# Controller with custom locations, set these as necessary before running
+# the deploy hook. (Defaults shown below.)
+#
+# Settings for Unifi Controller:
+# Location of Java keystore or unifi.keystore.jks file:
 #DEPLOY_UNIFI_KEYSTORE="/usr/lib/unifi/data/keystore"
+# Keystore password (built into Unifi Controller, not a user-set password):
 #DEPLOY_UNIFI_KEYPASS="aircontrolenterprise"
+# Command to restart Unifi Controller:
 #DEPLOY_UNIFI_RELOAD="service unifi restart"
+#
+# Settings for Unifi Cloud Key Gen1 (nginx admin pages):
+# Directory where cloudkey.crt and cloudkey.key live:
+#DEPLOY_UNIFI_CLOUDKEY_CERTDIR="/etc/ssl/private"
+# Command to restart maintenance pages and Controller
+# (same setting as above, default is updated when running on Cloud Key Gen1):
+#DEPLOY_UNIFI_RELOAD="service nginx restart && service unifi restart"
+#
+# Settings for UnifiOS (Cloud Key Gen2):
+# Directory where unifi-core.crt and unifi-core.key live:
+#DEPLOY_UNIFI_CORE_CONFIG="/data/unifi-core/config/"
+# Command to restart unifi-core:
+#DEPLOY_UNIFI_RELOAD="systemctl restart unifi-core"
+#
+# At least one of DEPLOY_UNIFI_KEYSTORE, DEPLOY_UNIFI_CLOUDKEY_CERTDIR,
+# or DEPLOY_UNIFI_CORE_CONFIG must exist to receive the deployed certs.
 
 ########  Public functions #####################
 
@@ -24,77 +55,160 @@ unifi_deploy() {
   _debug _cca "$_cca"
   _debug _cfullchain "$_cfullchain"
 
-  if ! _exists keytool; then
-    _err "keytool not found"
-    return 1
-  fi
+  _getdeployconf DEPLOY_UNIFI_KEYSTORE
+  _getdeployconf DEPLOY_UNIFI_KEYPASS
+  _getdeployconf DEPLOY_UNIFI_CLOUDKEY_CERTDIR
+  _getdeployconf DEPLOY_UNIFI_CORE_CONFIG
+  _getdeployconf DEPLOY_UNIFI_RELOAD
+
+  _debug2 DEPLOY_UNIFI_KEYSTORE "$DEPLOY_UNIFI_KEYSTORE"
+  _debug2 DEPLOY_UNIFI_KEYPASS "$DEPLOY_UNIFI_KEYPASS"
+  _debug2 DEPLOY_UNIFI_CLOUDKEY_CERTDIR "$DEPLOY_UNIFI_CLOUDKEY_CERTDIR"
+  _debug2 DEPLOY_UNIFI_CORE_CONFIG "$DEPLOY_UNIFI_CORE_CONFIG"
+  _debug2 DEPLOY_UNIFI_RELOAD "$DEPLOY_UNIFI_RELOAD"
+
+  # Space-separated list of environments detected and installed:
+  _services_updated=""
+
+  # Default reload commands accumulated as we auto-detect environments:
+  _reload_cmd=""
+
+  # Unifi Controller environment (self hosted or any Cloud Key) --
+  # auto-detect by file /usr/lib/unifi/data/keystore:
+  _unifi_keystore="${DEPLOY_UNIFI_KEYSTORE:-/usr/lib/unifi/data/keystore}"
+  if [ -f "$_unifi_keystore" ]; then
+    _info "Installing certificate for Unifi Controller (Java keystore)"
+    _debug _unifi_keystore "$_unifi_keystore"
+    if ! _exists keytool; then
+      _err "keytool not found"
+      return 1
+    fi
+    if [ ! -w "$_unifi_keystore" ]; then
+      _err "The file $_unifi_keystore is not writable, please change the permission."
+      return 1
+    fi
+
+    _unifi_keypass="${DEPLOY_UNIFI_KEYPASS:-aircontrolenterprise}"
 
-  DEFAULT_UNIFI_KEYSTORE="/usr/lib/unifi/data/keystore"
-  _unifi_keystore="${DEPLOY_UNIFI_KEYSTORE:-$DEFAULT_UNIFI_KEYSTORE}"
-  DEFAULT_UNIFI_KEYPASS="aircontrolenterprise"
-  _unifi_keypass="${DEPLOY_UNIFI_KEYPASS:-$DEFAULT_UNIFI_KEYPASS}"
-  DEFAULT_UNIFI_RELOAD="service unifi restart"
-  _reload="${DEPLOY_UNIFI_RELOAD:-$DEFAULT_UNIFI_RELOAD}"
-
-  _debug _unifi_keystore "$_unifi_keystore"
-  if [ ! -f "$_unifi_keystore" ]; then
-    if [ -z "$DEPLOY_UNIFI_KEYSTORE" ]; then
-      _err "unifi keystore is not found, please define DEPLOY_UNIFI_KEYSTORE"
+    _debug "Generate import pkcs12"
+    _import_pkcs12="$(_mktemp)"
+    _toPkcs "$_import_pkcs12" "$_ckey" "$_ccert" "$_cca" "$_unifi_keypass" unifi root
+    # shellcheck disable=SC2181
+    if [ "$?" != "0" ]; then
+      _err "Error generating pkcs12. Please re-run with --debug and report a bug."
       return 1
+    fi
+
+    _debug "Import into keystore: $_unifi_keystore"
+    if keytool -importkeystore \
+      -deststorepass "$_unifi_keypass" -destkeypass "$_unifi_keypass" -destkeystore "$_unifi_keystore" \
+      -srckeystore "$_import_pkcs12" -srcstoretype PKCS12 -srcstorepass "$_unifi_keypass" \
+      -alias unifi -noprompt; then
+      _debug "Import keystore success!"
+      rm "$_import_pkcs12"
     else
-      _err "It seems that the specified unifi keystore is not valid, please check."
+      _err "Error importing into Unifi Java keystore."
+      _err "Please re-run with --debug and report a bug."
+      rm "$_import_pkcs12"
       return 1
     fi
+
+    if systemctl -q is-active unifi; then
+      _reload_cmd="${_reload_cmd:+$_reload_cmd && }service unifi restart"
+    fi
+    _services_updated="${_services_updated} unifi"
+    _info "Install Unifi Controller certificate success!"
+  elif [ "$DEPLOY_UNIFI_KEYSTORE" ]; then
+    _err "The specified DEPLOY_UNIFI_KEYSTORE='$DEPLOY_UNIFI_KEYSTORE' is not valid, please check."
+    return 1
   fi
-  if [ ! -w "$_unifi_keystore" ]; then
-    _err "The file $_unifi_keystore is not writable, please change the permission."
+
+  # Cloud Key environment (non-UnifiOS -- nginx serves admin pages) --
+  # auto-detect by file /etc/ssl/private/cloudkey.key:
+  _cloudkey_certdir="${DEPLOY_UNIFI_CLOUDKEY_CERTDIR:-/etc/ssl/private}"
+  if [ -f "${_cloudkey_certdir}/cloudkey.key" ]; then
+    _info "Installing certificate for Cloud Key Gen1 (nginx admin pages)"
+    _debug _cloudkey_certdir "$_cloudkey_certdir"
+    if [ ! -w "$_cloudkey_certdir" ]; then
+      _err "The directory $_cloudkey_certdir is not writable; please check permissions."
+      return 1
+    fi
+    # Cloud Key expects to load the keystore from /etc/ssl/private/unifi.keystore.jks.
+    # Normally /usr/lib/unifi/data/keystore is a symlink there (so the keystore was
+    # updated above), but if not, we don't know how to handle this installation:
+    if ! cmp -s "$_unifi_keystore" "${_cloudkey_certdir}/unifi.keystore.jks"; then
+      _err "Unsupported Cloud Key configuration: keystore not found at '${_cloudkey_certdir}/unifi.keystore.jks'"
+      return 1
+    fi
+
+    cat "$_cfullchain" >"${_cloudkey_certdir}/cloudkey.crt"
+    cat "$_ckey" >"${_cloudkey_certdir}/cloudkey.key"
+    (cd "$_cloudkey_certdir" && tar -cf cert.tar cloudkey.crt cloudkey.key unifi.keystore.jks)
+
+    if systemctl -q is-active nginx; then
+      _reload_cmd="${_reload_cmd:+$_reload_cmd && }service nginx restart"
+    fi
+    _info "Install Cloud Key Gen1 certificate success!"
+    _services_updated="${_services_updated} nginx"
+  elif [ "$DEPLOY_UNIFI_CLOUDKEY_CERTDIR" ]; then
+    _err "The specified DEPLOY_UNIFI_CLOUDKEY_CERTDIR='$DEPLOY_UNIFI_CLOUDKEY_CERTDIR' is not valid, please check."
     return 1
   fi
 
-  _info "Generate import pkcs12"
-  _import_pkcs12="$(_mktemp)"
-  _toPkcs "$_import_pkcs12" "$_ckey" "$_ccert" "$_cca" "$_unifi_keypass" unifi root
-  if [ "$?" != "0" ]; then
-    _err "Oops, error creating import pkcs12, please report bug to us."
+  # UnifiOS environment -- auto-detect by /data/unifi-core/config/unifi-core.key:
+  _unifi_core_config="${DEPLOY_UNIFI_CORE_CONFIG:-/data/unifi-core/config}"
+  if [ -f "${_unifi_core_config}/unifi-core.key" ]; then
+    _info "Installing certificate for UnifiOS"
+    _debug _unifi_core_config "$_unifi_core_config"
+    if [ ! -w "$_unifi_core_config" ]; then
+      _err "The directory $_unifi_core_config is not writable; please check permissions."
+      return 1
+    fi
+
+    cat "$_cfullchain" >"${_unifi_core_config}/unifi-core.crt"
+    cat "$_ckey" >"${_unifi_core_config}/unifi-core.key"
+
+    if systemctl -q is-active unifi-core; then
+      _reload_cmd="${_reload_cmd:+$_reload_cmd && }systemctl restart unifi-core"
+    fi
+    _info "Install UnifiOS certificate success!"
+    _services_updated="${_services_updated} unifi-core"
+  elif [ "$DEPLOY_UNIFI_CORE_CONFIG" ]; then
+    _err "The specified DEPLOY_UNIFI_CORE_CONFIG='$DEPLOY_UNIFI_CORE_CONFIG' is not valid, please check."
     return 1
   fi
 
-  _info "Modify unifi keystore: $_unifi_keystore"
-  if keytool -importkeystore \
-    -deststorepass "$_unifi_keypass" -destkeypass "$_unifi_keypass" -destkeystore "$_unifi_keystore" \
-    -srckeystore "$_import_pkcs12" -srcstoretype PKCS12 -srcstorepass "$_unifi_keypass" \
-    -alias unifi -noprompt; then
-    _info "Import keystore success!"
-    rm "$_import_pkcs12"
-  else
-    _err "Import unifi keystore error, please report bug to us."
-    rm "$_import_pkcs12"
+  if [ -z "$_services_updated" ]; then
+    # None of the Unifi environments were auto-detected, so no deployment has occurred
+    # (and none of DEPLOY_UNIFI_{KEYSTORE,CLOUDKEY_CERTDIR,CORE_CONFIG} were set).
+    _err "Unable to detect Unifi environment in standard location."
+    _err "(This deploy hook must be run on the Unifi device, not a remote machine.)"
+    _err "For non-standard Unifi installations, set DEPLOY_UNIFI_KEYSTORE,"
+    _err "DEPLOY_UNIFI_CLOUDKEY_CERTDIR, and/or DEPLOY_UNIFI_CORE_CONFIG as appropriate."
     return 1
   fi
 
-  _info "Run reload: $_reload"
-  if eval "$_reload"; then
+  _reload_cmd="${DEPLOY_UNIFI_RELOAD:-$_reload_cmd}"
+  if [ -z "$_reload_cmd" ]; then
+    _err "Certificates were installed for services:${_services_updated},"
+    _err "but none appear to be active. Please set DEPLOY_UNIFI_RELOAD"
+    _err "to a command that will restart the necessary services."
+    return 1
+  fi
+  _info "Reload services (this may take some time): $_reload_cmd"
+  if eval "$_reload_cmd"; then
     _info "Reload success!"
-    if [ "$DEPLOY_UNIFI_KEYSTORE" ]; then
-      _savedomainconf DEPLOY_UNIFI_KEYSTORE "$DEPLOY_UNIFI_KEYSTORE"
-    else
-      _cleardomainconf DEPLOY_UNIFI_KEYSTORE
-    fi
-    if [ "$DEPLOY_UNIFI_KEYPASS" ]; then
-      _savedomainconf DEPLOY_UNIFI_KEYPASS "$DEPLOY_UNIFI_KEYPASS"
-    else
-      _cleardomainconf DEPLOY_UNIFI_KEYPASS
-    fi
-    if [ "$DEPLOY_UNIFI_RELOAD" ]; then
-      _savedomainconf DEPLOY_UNIFI_RELOAD "$DEPLOY_UNIFI_RELOAD"
-    else
-      _cleardomainconf DEPLOY_UNIFI_RELOAD
-    fi
-    return 0
   else
     _err "Reload error"
     return 1
   fi
-  return 0
 
+  # Successful, so save all (non-default) config:
+  _savedeployconf DEPLOY_UNIFI_KEYSTORE "$DEPLOY_UNIFI_KEYSTORE"
+  _savedeployconf DEPLOY_UNIFI_KEYPASS "$DEPLOY_UNIFI_KEYPASS"
+  _savedeployconf DEPLOY_UNIFI_CLOUDKEY_CERTDIR "$DEPLOY_UNIFI_CLOUDKEY_CERTDIR"
+  _savedeployconf DEPLOY_UNIFI_CORE_CONFIG "$DEPLOY_UNIFI_CORE_CONFIG"
+  _savedeployconf DEPLOY_UNIFI_RELOAD "$DEPLOY_UNIFI_RELOAD"
+
+  return 0
 }
index ca1f56c7e3a33555e0ebb186a78df16a45e7266d..4c9217e5828dd93804328b4d45d455b933102251 100644 (file)
@@ -1,10 +1,9 @@
 #!/usr/bin/env sh
 
-#Arvan_Token="xxxx"
+#Arvan_Token="Apikey xxxx"
 
 ARVAN_API_URL="https://napi.arvancloud.com/cdn/4.0/domains"
-
-#Author: Ehsan Aliakbar
+#Author: Vahid Fardi
 #Report Bugs here: https://github.com/Neilpang/acme.sh
 #
 ########  Public functions #####################
@@ -38,6 +37,7 @@ dns_arvan_add() {
   _info "Adding record"
   if _arvan_rest POST "$_domain/dns-records" "{\"type\":\"TXT\",\"name\":\"$_sub_domain\",\"value\":{\"text\":\"$txtvalue\"},\"ttl\":120}"; then
     if _contains "$response" "$txtvalue"; then
+      _info "response id is $response"
       _info "Added, OK"
       return 0
     elif _contains "$response" "Record Data is Duplicated"; then
@@ -49,7 +49,7 @@ dns_arvan_add() {
     fi
   fi
   _err "Add txt record error."
-  return 1
+  return 0
 }
 
 #Usage: fulldomain txtvalue
@@ -73,33 +73,21 @@ dns_arvan_rm() {
   _debug _domain "$_domain"
 
   _debug "Getting txt records"
-  shorted_txtvalue=$(printf "%s" "$txtvalue" | cut -d "-" -d "_" -f1)
-  _arvan_rest GET "${_domain}/dns-records?search=$shorted_txtvalue"
-
+  _arvan_rest GET "${_domain}/dns-records"
   if ! printf "%s" "$response" | grep \"current_page\":1 >/dev/null; then
     _err "Error on Arvan Api"
     _err "Please create a github issue with debbug log"
     return 1
   fi
 
-  count=$(printf "%s\n" "$response" | _egrep_o "\"total\":[^,]*" | cut -d : -f 2)
-  _debug count "$count"
-  if [ "$count" = "0" ]; then
-    _info "Don't need to remove."
-  else
-    record_id=$(printf "%s\n" "$response" | _egrep_o "\"id\":\"[^\"]*\"" | cut -d : -f 2 | tr -d \" | head -n 1)
-    _debug "record_id" "$record_id"
-    if [ -z "$record_id" ]; then
-      _err "Can not get record id to remove."
-      return 1
-    fi
-    if ! _arvan_rest "DELETE" "${_domain}/dns-records/$record_id"; then
-      _err "Delete record error."
-      return 1
-    fi
-    _debug "$response"
-    _contains "$response" 'dns record deleted'
+  _record_id=$(echo "$response" | _egrep_o ".\"id\":\"[^\"]*\",\"type\":\"txt\",\"name\":\"_acme-challenge\",\"value\":{\"text\":\"$txtvalue\"}" | cut -d : -f 2 | cut -d , -f 1 | tr -d \")
+  if ! _arvan_rest "DELETE" "${_domain}/dns-records/${_record_id}"; then
+    _err "Error on Arvan Api"
+    return 1
   fi
+  _debug "$response"
+  _contains "$response" 'dns record deleted'
+  return 0
 }
 
 ####################  Private functions below ##################################
@@ -111,7 +99,7 @@ dns_arvan_rm() {
 # _domain_id=sdjkglgdfewsdfg
 _get_root() {
   domain=$1
-  i=1
+  i=2
   p=1
   while true; do
     h=$(printf "%s" "$domain" | cut -d . -f $i-100)
@@ -121,12 +109,11 @@ _get_root() {
       return 1
     fi
 
-    if ! _arvan_rest GET "?search=$h"; then
+    if ! _arvan_rest GET "$h"; then
       return 1
     fi
-
-    if _contains "$response" "\"domain\":\"$h\"" || _contains "$response" '"total":1'; then
-      _domain_id=$(echo "$response" | _egrep_o "\[.\"id\":\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \")
+    if _contains "$response" "\"domain\":\"$h\""; then
+      _domain_id=$(echo "$response" | cut -d : -f 3 | cut -d , -f 1 | tr -d \")
       if [ "$_domain_id" ]; then
         _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
         _domain=$h
@@ -146,7 +133,6 @@ _arvan_rest() {
   data="$3"
 
   token_trimmed=$(echo "$Arvan_Token" | tr -d '"')
-
   export _H1="Authorization: $token_trimmed"
 
   if [ "$mtd" = "DELETE" ]; then
@@ -160,4 +146,5 @@ _arvan_rest() {
   else
     response="$(_get "$ARVAN_API_URL/$ep$data")"
   fi
+  return 0
 }
index 033fa5aae066f3a8e6e6fd013f4896f5776f63af..9b8b7a8b83e033b76652f09bc5777b2411a680c4 100755 (executable)
@@ -89,7 +89,7 @@ add_record() {
 
   _info "Adding record"
 
-  if ! _rest POST "Record.Create" "login_token=$DP_Id,$DP_Key&format=json&lang=en&domain_id=$_domain_id&sub_domain=$_sub_domain&record_type=TXT&value=$txtvalue&record_line=默认"; then
+  if ! _rest POST "Record.Create" "login_token=$DP_Id,$DP_Key&format=json&lang=en&domain_id=$_domain_id&sub_domain=$_sub_domain&record_type=TXT&value=$txtvalue&record_line=%E9%BB%98%E8%AE%A4"; then
     return 1
   fi
 
index 618e12c66154fa2b7bf061a62371348d22c39a5d..d6e1dbdceabb5bbe32d65f15e60c0897135a12a9 100755 (executable)
@@ -12,7 +12,7 @@
 
 DuckDNS_API="https://www.duckdns.org/update"
 
-########  Public functions #####################
+########  Public functions ######################
 
 #Usage: dns_duckdns_add _acme-challenge.domain.duckdns.org "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
 dns_duckdns_add() {
@@ -112,7 +112,7 @@ _duckdns_rest() {
   param="$2"
   _debug param "$param"
   url="$DuckDNS_API?$param"
-  if [ "$DEBUG" -gt 0 ]; then
+  if [ -n "$DEBUG" ] && [ "$DEBUG" -gt 0 ]; then
     url="$url&verbose=true"
   fi
   _debug url "$url"
@@ -121,7 +121,7 @@ _duckdns_rest() {
   if [ "$method" = "GET" ]; then
     response="$(_get "$url")"
     _debug2 response "$response"
-    if [ "$DEBUG" -gt 0 ] && _contains "$response" "UPDATED" && _contains "$response" "OK"; then
+    if [ -n "$DEBUG" ] && [ "$DEBUG" -gt 0 ] && _contains "$response" "UPDATED" && _contains "$response" "OK"; then
       response="OK"
     fi
   else
index 74fec2a96c81439f8e6214837bc409d54aadc982..f7192725e2cb1f263c2a9a38aa9a9a344e7bd68f 100644 (file)
@@ -5,7 +5,7 @@
 # HUAWEICLOUD_ProjectID
 
 iam_api="https://iam.myhuaweicloud.com"
-dns_api="https://dns.ap-southeast-1.myhuaweicloud.com"
+dns_api="https://dns.ap-southeast-1.myhuaweicloud.com" # Should work
 
 ########  Public functions #####################
 
@@ -29,16 +29,27 @@ dns_huaweicloud_add() {
     return 1
   fi
 
+  unset token # Clear token
   token="$(_get_token "${HUAWEICLOUD_Username}" "${HUAWEICLOUD_Password}" "${HUAWEICLOUD_ProjectID}")"
-  _debug2 "${token}"
+  if [ -z "${token}" ]; then # Check token
+    _err "dns_api(dns_huaweicloud): Error getting token."
+    return 1
+  fi
+  _debug "Access token is: ${token}"
+
+  unset zoneid
   zoneid="$(_get_zoneid "${token}" "${fulldomain}")"
-  _debug "${zoneid}"
+  if [ -z "${zoneid}" ]; then
+    _err "dns_api(dns_huaweicloud): Error getting zone id."
+    return 1
+  fi
+  _debug "Zone ID is: ${zoneid}"
 
   _debug "Adding Record"
   _add_record "${token}" "${fulldomain}" "${txtvalue}"
   ret="$?"
   if [ "${ret}" != "0" ]; then
-    _err "dns_huaweicloud: Error adding record."
+    _err "dns_api(dns_huaweicloud): Error adding record."
     return 1
   fi
 
@@ -69,12 +80,21 @@ dns_huaweicloud_rm() {
     return 1
   fi
 
+  unset token # Clear token
   token="$(_get_token "${HUAWEICLOUD_Username}" "${HUAWEICLOUD_Password}" "${HUAWEICLOUD_ProjectID}")"
-  _debug2 "${token}"
+  if [ -z "${token}" ]; then # Check token
+    _err "dns_api(dns_huaweicloud): Error getting token."
+    return 1
+  fi
+  _debug "Access token is: ${token}"
+
+  unset zoneid
   zoneid="$(_get_zoneid "${token}" "${fulldomain}")"
-  _debug "${zoneid}"
-  record_id="$(_get_recordset_id "${token}" "${fulldomain}" "${zoneid}")"
-  _debug "Record Set ID is: ${record_id}"
+  if [ -z "${zoneid}" ]; then
+    _err "dns_api(dns_huaweicloud): Error getting zone id."
+    return 1
+  fi
+  _debug "Zone ID is: ${zoneid}"
 
   # Remove all records
   # Therotically HuaweiCloud does not allow more than one record set
index e6bd5000b2564dba519d9467a9f4c659388300a6..aaf8580fded9f372c7d9fe5e549f07d3f37c9db4 100755 (executable)
@@ -24,20 +24,9 @@ dns_ionos_add() {
     return 1
   fi
 
-  _new_record="{\"name\":\"$_sub_domain.$_domain\",\"type\":\"TXT\",\"content\":\"$txtvalue\",\"ttl\":$IONOS_TXT_TTL,\"prio\":$IONOS_TXT_PRIO,\"disabled\":false}"
+  _body="[{\"name\":\"$_sub_domain.$_domain\",\"type\":\"TXT\",\"content\":\"$txtvalue\",\"ttl\":$IONOS_TXT_TTL,\"prio\":$IONOS_TXT_PRIO,\"disabled\":false}]"
 
-  # As no POST route is supported by the API, check for existing records and include them in the PATCH request in order not delete them.
-  # This is required to support ACME v2 wildcard certificate creation, where two TXT records for the same domain name are created.
-
-  _ionos_get_existing_records "$fulldomain" "$_zone_id"
-
-  if [ "$_existing_records" ]; then
-    _body="[$_new_record,$_existing_records]"
-  else
-    _body="[$_new_record]"
-  fi
-
-  if _ionos_rest PATCH "$IONOS_ROUTE_ZONES/$_zone_id" "$_body" && [ -z "$response" ]; then
+  if _ionos_rest POST "$IONOS_ROUTE_ZONES/$_zone_id/records" "$_body" && [ -z "$response" ]; then
     _info "TXT record has been created successfully."
     return 0
   fi
@@ -125,17 +114,6 @@ _get_root() {
   return 1
 }
 
-_ionos_get_existing_records() {
-  fulldomain=$1
-  zone_id=$2
-
-  if _ionos_rest GET "$IONOS_ROUTE_ZONES/$zone_id?recordName=$fulldomain&recordType=TXT"; then
-    response="$(echo "$response" | tr -d "\n")"
-
-    _existing_records="$(printf "%s\n" "$response" | _egrep_o "\"records\":\[.*\]" | _head_n 1 | cut -d '[' -f 2 | sed 's/]//')"
-  fi
-}
-
 _ionos_get_record() {
   fulldomain=$1
   zone_id=$2
@@ -168,7 +146,7 @@ _ionos_rest() {
     export _H2="Accept: application/json"
     export _H3="Content-Type: application/json"
 
-    response="$(_post "$data" "$IONOS_API$route" "" "$method")"
+    response="$(_post "$data" "$IONOS_API$route" "" "$method" "application/json")"
   else
     export _H2="Accept: */*"
 
index 7ce39fa951d291ccc5bea4226f755718a970c330..d15d6b0e74e5ef952e2f91039d53092105751635 100755 (executable)
@@ -208,7 +208,7 @@ _namecheap_parse_host() {
   _hostid=$(echo "$_host" | _egrep_o ' HostId="[^"]*' | cut -d '"' -f 2)
   _hostname=$(echo "$_host" | _egrep_o ' Name="[^"]*' | cut -d '"' -f 2)
   _hosttype=$(echo "$_host" | _egrep_o ' Type="[^"]*' | cut -d '"' -f 2)
-  _hostaddress=$(echo "$_host" | _egrep_o ' Address="[^"]*' | cut -d '"' -f 2)
+  _hostaddress=$(echo "$_host" | _egrep_o ' Address="[^"]*' | cut -d '"' -f 2 | _xml_decode)
   _hostmxpref=$(echo "$_host" | _egrep_o ' MXPref="[^"]*' | cut -d '"' -f 2)
   _hostttl=$(echo "$_host" | _egrep_o ' TTL="[^"]*' | cut -d '"' -f 2)
 
@@ -405,3 +405,7 @@ _namecheap_set_tld_sld() {
   done
 
 }
+
+_xml_decode() {
+  sed 's/&quot;/"/g'
+}
index 8f07e8c4a6a44a3a0afc7d2637db2c30de7e1cdb..28b35492afc3da948123dcb3db982d96b7def360 100755 (executable)
@@ -175,13 +175,13 @@ _get_root() {
   i=1
 
   if _pdns_rest "GET" "/api/v1/servers/$PDNS_ServerId/zones"; then
-    _zones_response="$response"
+    _zones_response=$(echo "$response" | _normalizeJson)
   fi
 
   while true; do
     h=$(printf "%s" "$domain" | cut -d . -f $i-100)
 
-    if _contains "$_zones_response" "\"name\": \"$h.\""; then
+    if _contains "$_zones_response" "\"name\":\"$h.\""; then
       _domain="$h."
       if [ -z "$h" ]; then
         _domain="=2E"
diff --git a/dnsapi/dns_porkbun.sh b/dnsapi/dns_porkbun.sh
new file mode 100644 (file)
index 0000000..18da6b2
--- /dev/null
@@ -0,0 +1,157 @@
+#!/usr/bin/env sh
+
+#
+#PORKBUN_API_KEY="pk1_0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
+#PORKBUN_SECRET_API_KEY="sk1_0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
+
+PORKBUN_Api="https://porkbun.com/api/json/v3"
+
+########  Public functions #####################
+
+#Usage: add  _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+dns_porkbun_add() {
+  fulldomain=$1
+  txtvalue=$2
+
+  PORKBUN_API_KEY="${PORKBUN_API_KEY:-$(_readaccountconf_mutable PORKBUN_API_KEY)}"
+  PORKBUN_SECRET_API_KEY="${PORKBUN_SECRET_API_KEY:-$(_readaccountconf_mutable PORKBUN_SECRET_API_KEY)}"
+
+  if [ -z "$PORKBUN_API_KEY" ] || [ -z "$PORKBUN_SECRET_API_KEY" ]; then
+    PORKBUN_API_KEY=''
+    PORKBUN_SECRET_API_KEY=''
+    _err "You didn't specify a Porkbun api key and secret api key yet."
+    _err "You can get yours from here https://porkbun.com/account/api."
+    return 1
+  fi
+
+  #save the credentials to the account conf file.
+  _saveaccountconf_mutable PORKBUN_API_KEY "$PORKBUN_API_KEY"
+  _saveaccountconf_mutable PORKBUN_SECRET_API_KEY "$PORKBUN_SECRET_API_KEY"
+
+  _debug 'First detect the root zone'
+  if ! _get_root "$fulldomain"; then
+    return 1
+  fi
+  _debug _sub_domain "$_sub_domain"
+  _debug _domain "$_domain"
+
+  # For wildcard cert, the main root domain and the wildcard domain have the same txt subdomain name, so
+  # we can not use updating anymore.
+  #  count=$(printf "%s\n" "$response" | _egrep_o "\"count\":[^,]*" | cut -d : -f 2)
+  #  _debug count "$count"
+  #  if [ "$count" = "0" ]; then
+  _info "Adding record"
+  if _porkbun_rest POST "dns/create/$_domain" "{\"name\":\"$_sub_domain\",\"type\":\"TXT\",\"content\":\"$txtvalue\",\"ttl\":120}"; then
+    if _contains "$response" '\"status\":"SUCCESS"'; then
+      _info "Added, OK"
+      return 0
+    elif _contains "$response" "The record already exists"; then
+      _info "Already exists, OK"
+      return 0
+    else
+      _err "Add txt record error. ($response)"
+      return 1
+    fi
+  fi
+  _err "Add txt record error."
+  return 1
+
+}
+
+#fulldomain txtvalue
+dns_porkbun_rm() {
+  fulldomain=$1
+  txtvalue=$2
+
+  PORKBUN_API_KEY="${PORKBUN_API_KEY:-$(_readaccountconf_mutable PORKBUN_API_KEY)}"
+  PORKBUN_SECRET_API_KEY="${PORKBUN_SECRET_API_KEY:-$(_readaccountconf_mutable PORKBUN_SECRET_API_KEY)}"
+
+  _debug 'First detect the root zone'
+  if ! _get_root "$fulldomain"; then
+    return 1
+  fi
+  _debug _sub_domain "$_sub_domain"
+  _debug _domain "$_domain"
+
+  count=$(echo "$response" | _egrep_o "\"count\": *[^,]*" | cut -d : -f 2 | tr -d " ")
+  _debug count "$count"
+  if [ "$count" = "0" ]; then
+    _info "Don't need to remove."
+  else
+    record_id=$(echo "$response" | tr '{' '\n' | grep "$txtvalue" | cut -d, -f1 | cut -d: -f2 | tr -d \")
+    _debug "record_id" "$record_id"
+    if [ -z "$record_id" ]; then
+      _err "Can not get record id to remove."
+      return 1
+    fi
+    if ! _porkbun_rest POST "dns/delete/$_domain/$record_id"; then
+      _err "Delete record error."
+      return 1
+    fi
+    echo "$response" | tr -d " " | grep '\"status\":"SUCCESS"' >/dev/null
+  fi
+
+}
+
+####################  Private functions below ##################################
+#_acme-challenge.www.domain.com
+#returns
+# _sub_domain=_acme-challenge.www
+# _domain=domain.com
+_get_root() {
+  domain=$1
+  i=1
+  while true; do
+    h=$(printf "%s" "$domain" | cut -d . -f $i-100)
+    _debug h "$h"
+    if [ -z "$h" ]; then
+      return 1
+    fi
+
+    if _porkbun_rest POST "dns/retrieve/$h"; then
+      if _contains "$response" "\"status\":\"SUCCESS\""; then
+        _sub_domain="$(echo "$fulldomain" | sed "s/\\.$_domain\$//")"
+        _domain=$h
+        return 0
+      else
+        _debug "Go to next level of $_domain"
+      fi
+    else
+      _debug "Go to next level of $_domain"
+    fi
+    i=$(_math "$i" + 1)
+  done
+
+  return 1
+}
+
+_porkbun_rest() {
+  m=$1
+  ep="$2"
+  data="$3"
+  _debug "$ep"
+
+  api_key_trimmed=$(echo "$PORKBUN_API_KEY" | tr -d '"')
+  secret_api_key_trimmed=$(echo "$PORKBUN_SECRET_API_KEY" | tr -d '"')
+
+  test -z "$data" && data="{" || data="$(echo $data | cut -d'}' -f1),"
+  data="$data\"apikey\":\"$api_key_trimmed\",\"secretapikey\":\"$secret_api_key_trimmed\"}"
+
+  export _H1="Content-Type: application/json"
+
+  if [ "$m" != "GET" ]; then
+    _debug data "$data"
+    response="$(_post "$data" "$PORKBUN_Api/$ep" "" "$m")"
+  else
+    response="$(_get "$PORKBUN_Api/$ep")"
+  fi
+
+  _sleep 3 # prevent rate limit
+
+  if [ "$?" != "0" ]; then
+    _err "error $ep"
+    return 1
+  fi
+  _debug2 response "$response"
+  return 0
+}
index e73d85b0977744a1f848826b2bbef1449b9d4a59..f70a2294434fcfd9c5a59010cf4150b2a41049b0 100755 (executable)
@@ -49,16 +49,42 @@ dns_servercow_add() {
   _debug _sub_domain "$_sub_domain"
   _debug _domain "$_domain"
 
-  if _servercow_api POST "$_domain" "{\"type\":\"TXT\",\"name\":\"$fulldomain\",\"content\":\"$txtvalue\",\"ttl\":20}"; then
-    if printf -- "%s" "$response" | grep "ok" >/dev/null; then
-      _info "Added, OK"
-      return 0
-    else
-      _err "add txt record error."
-      return 1
+  # check whether a txt record already exists for the subdomain
+  if printf -- "%s" "$response" | grep "{\"name\":\"$_sub_domain\",\"ttl\":20,\"type\":\"TXT\"" >/dev/null; then
+    _info "A txt record with the same name already exists."
+    # trim the string on the left
+    txtvalue_old=${response#*{\"name\":\"$_sub_domain\",\"ttl\":20,\"type\":\"TXT\",\"content\":\"}
+    # trim the string on the right
+    txtvalue_old=${txtvalue_old%%\"*}
+
+    _debug txtvalue_old "$txtvalue_old"
+
+    _info "Add the new txtvalue to the existing txt record."
+    if _servercow_api POST "$_domain" "{\"type\":\"TXT\",\"name\":\"$fulldomain\",\"content\":[\"$txtvalue\",\"$txtvalue_old\"],\"ttl\":20}"; then
+      if printf -- "%s" "$response" | grep "ok" >/dev/null; then
+        _info "Added additional txtvalue, OK"
+        return 0
+      else
+        _err "add txt record error."
+        return 1
+      fi
     fi
+    _err "add txt record error."
+    return 1
+  else
+    _info "There is no txt record with the name yet."
+    if _servercow_api POST "$_domain" "{\"type\":\"TXT\",\"name\":\"$fulldomain\",\"content\":\"$txtvalue\",\"ttl\":20}"; then
+      if printf -- "%s" "$response" | grep "ok" >/dev/null; then
+        _info "Added, OK"
+        return 0
+      else
+        _err "add txt record error."
+        return 1
+      fi
+    fi
+    _err "add txt record error."
+    return 1
   fi
-  _err "add txt record error."
 
   return 1
 }
index d053dcf6cea518403f84b6b3f8be5754cc9950b4..e0e0501761ae389c2ad67aa20c31bc75a91137da 100644 (file)
@@ -6,9 +6,11 @@
 #SIMPLY_ApiKey="apikey"
 #
 #SIMPLY_Api="https://api.simply.com/1/[ACCOUNTNAME]/[APIKEY]"
-
 SIMPLY_Api_Default="https://api.simply.com/1"
 
+#This is used for determining success of REST call
+SIMPLY_SUCCESS_CODE='"status": 200'
+
 ########  Public functions #####################
 #Usage: add  _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
 dns_simply_add() {
@@ -171,7 +173,7 @@ _get_root() {
       return 1
     fi
 
-    if _contains "$response" '"code":"NOT_FOUND"'; then
+    if ! _contains "$response" "$SIMPLY_SUCCESS_CODE"; then
       _debug "$h not found"
     else
       _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
@@ -196,6 +198,12 @@ _simply_add_record() {
     return 1
   fi
 
+  if ! _contains "$response" "$SIMPLY_SUCCESS_CODE"; then
+    _err "Call to API not sucessfull, see below message for more details"
+    _err "$response"
+    return 1
+  fi
+
   return 0
 }
 
@@ -211,6 +219,12 @@ _simply_delete_record() {
     return 1
   fi
 
+  if ! _contains "$response" "$SIMPLY_SUCCESS_CODE"; then
+    _err "Call to API not sucessfull, see below message for more details"
+    _err "$response"
+    return 1
+  fi
+
   return 0
 }
 
index 6aa37ca33b14a57aabf44ced3f5acb27c9917bd8..293c665ea7bc5442651a030034557147c68a14cd 100644 (file)
 
 # support smtp
 
+# Please report bugs to https://github.com/acmesh-official/acme.sh/issues/3358
+
+# This implementation uses either curl or Python (3 or 2.7).
+# (See also the "mail" notify hook, which supports other ways to send mail.)
+
+# SMTP_FROM="from@example.com"  # required
+# SMTP_TO="to@example.com"  # required
+# SMTP_HOST="smtp.example.com"  # required
+# SMTP_PORT="25"  # defaults to 25, 465 or 587 depending on SMTP_SECURE
+# SMTP_SECURE="tls"  # one of "none", "ssl" (implicit TLS, TLS Wrapper), "tls" (explicit TLS, STARTTLS)
+# SMTP_USERNAME=""  # set if SMTP server requires login
+# SMTP_PASSWORD=""  # set if SMTP server requires login
+# SMTP_TIMEOUT="30"  # seconds for SMTP operations to timeout
+# SMTP_BIN="/path/to/python_or_curl"  # default finds first of python3, python2.7, python, pypy3, pypy, curl on PATH
+
+SMTP_SECURE_DEFAULT="tls"
+SMTP_TIMEOUT_DEFAULT="30"
+
+# subject content statuscode
 smtp_send() {
-  _subject="$1"
-  _content="$2"
-  _statusCode="$3" #0: success, 1: error 2($RENEW_SKIP): skipped
-  _debug "_subject" "$_subject"
-  _debug "_content" "$_content"
-  _debug "_statusCode" "$_statusCode"
-
-  _err "Not implemented yet."
-  return 1
+  SMTP_SUBJECT="$1"
+  SMTP_CONTENT="$2"
+  # UNUSED: _statusCode="$3" # 0: success, 1: error 2($RENEW_SKIP): skipped
+
+  # Load and validate config:
+  SMTP_BIN="$(_readaccountconf_mutable_default SMTP_BIN)"
+  if [ -n "$SMTP_BIN" ] && ! _exists "$SMTP_BIN"; then
+    _err "SMTP_BIN '$SMTP_BIN' does not exist."
+    return 1
+  fi
+  if [ -z "$SMTP_BIN" ]; then
+    # Look for a command that can communicate with an SMTP server.
+    # (Please don't add sendmail, ssmtp, mutt, mail, or msmtp here.
+    # Those are already handled by the "mail" notify hook.)
+    for cmd in python3 python2.7 python pypy3 pypy curl; do
+      if _exists "$cmd"; then
+        SMTP_BIN="$cmd"
+        break
+      fi
+    done
+    if [ -z "$SMTP_BIN" ]; then
+      _err "The smtp notify-hook requires curl or Python, but can't find any."
+      _err 'If you have one of them, define SMTP_BIN="/path/to/curl_or_python".'
+      _err 'Otherwise, see if you can use the "mail" notify-hook instead.'
+      return 1
+    fi
+  fi
+  _debug SMTP_BIN "$SMTP_BIN"
+  _saveaccountconf_mutable_default SMTP_BIN "$SMTP_BIN"
+
+  SMTP_FROM="$(_readaccountconf_mutable_default SMTP_FROM)"
+  SMTP_FROM="$(_clean_email_header "$SMTP_FROM")"
+  if [ -z "$SMTP_FROM" ]; then
+    _err "You must define SMTP_FROM as the sender email address."
+    return 1
+  fi
+  if _email_has_display_name "$SMTP_FROM"; then
+    _err "SMTP_FROM must be only a simple email address (sender@example.com)."
+    _err "Change your SMTP_FROM='$SMTP_FROM' to remove the display name."
+    return 1
+  fi
+  _debug SMTP_FROM "$SMTP_FROM"
+  _saveaccountconf_mutable_default SMTP_FROM "$SMTP_FROM"
+
+  SMTP_TO="$(_readaccountconf_mutable_default SMTP_TO)"
+  SMTP_TO="$(_clean_email_header "$SMTP_TO")"
+  if [ -z "$SMTP_TO" ]; then
+    _err "You must define SMTP_TO as the recipient email address(es)."
+    return 1
+  fi
+  if _email_has_display_name "$SMTP_TO"; then
+    _err "SMTP_TO must be only simple email addresses (to@example.com,to2@example.com)."
+    _err "Change your SMTP_TO='$SMTP_TO' to remove the display name(s)."
+    return 1
+  fi
+  _debug SMTP_TO "$SMTP_TO"
+  _saveaccountconf_mutable_default SMTP_TO "$SMTP_TO"
+
+  SMTP_HOST="$(_readaccountconf_mutable_default SMTP_HOST)"
+  if [ -z "$SMTP_HOST" ]; then
+    _err "You must define SMTP_HOST as the SMTP server hostname."
+    return 1
+  fi
+  _debug SMTP_HOST "$SMTP_HOST"
+  _saveaccountconf_mutable_default SMTP_HOST "$SMTP_HOST"
+
+  SMTP_SECURE="$(_readaccountconf_mutable_default SMTP_SECURE "$SMTP_SECURE_DEFAULT")"
+  case "$SMTP_SECURE" in
+  "none") smtp_port_default="25" ;;
+  "ssl") smtp_port_default="465" ;;
+  "tls") smtp_port_default="587" ;;
+  *)
+    _err "Invalid SMTP_SECURE='$SMTP_SECURE'. It must be 'ssl', 'tls' or 'none'."
+    return 1
+    ;;
+  esac
+  _debug SMTP_SECURE "$SMTP_SECURE"
+  _saveaccountconf_mutable_default SMTP_SECURE "$SMTP_SECURE" "$SMTP_SECURE_DEFAULT"
+
+  SMTP_PORT="$(_readaccountconf_mutable_default SMTP_PORT "$smtp_port_default")"
+  case "$SMTP_PORT" in
+  *[!0-9]*)
+    _err "Invalid SMTP_PORT='$SMTP_PORT'. It must be a port number."
+    return 1
+    ;;
+  esac
+  _debug SMTP_PORT "$SMTP_PORT"
+  _saveaccountconf_mutable_default SMTP_PORT "$SMTP_PORT" "$smtp_port_default"
+
+  SMTP_USERNAME="$(_readaccountconf_mutable_default SMTP_USERNAME)"
+  _debug SMTP_USERNAME "$SMTP_USERNAME"
+  _saveaccountconf_mutable_default SMTP_USERNAME "$SMTP_USERNAME"
+
+  SMTP_PASSWORD="$(_readaccountconf_mutable_default SMTP_PASSWORD)"
+  _secure_debug SMTP_PASSWORD "$SMTP_PASSWORD"
+  _saveaccountconf_mutable_default SMTP_PASSWORD "$SMTP_PASSWORD"
+
+  SMTP_TIMEOUT="$(_readaccountconf_mutable_default SMTP_TIMEOUT "$SMTP_TIMEOUT_DEFAULT")"
+  _debug SMTP_TIMEOUT "$SMTP_TIMEOUT"
+  _saveaccountconf_mutable_default SMTP_TIMEOUT "$SMTP_TIMEOUT" "$SMTP_TIMEOUT_DEFAULT"
+
+  SMTP_X_MAILER="$(_clean_email_header "$PROJECT_NAME $VER --notify-hook smtp")"
+
+  # Run with --debug 2 (or above) to echo the transcript of the SMTP session.
+  # Careful: this may include SMTP_PASSWORD in plaintext!
+  if [ "${DEBUG:-$DEBUG_LEVEL_NONE}" -ge "$DEBUG_LEVEL_2" ]; then
+    SMTP_SHOW_TRANSCRIPT="True"
+  else
+    SMTP_SHOW_TRANSCRIPT=""
+  fi
+
+  SMTP_SUBJECT=$(_clean_email_header "$SMTP_SUBJECT")
+  _debug SMTP_SUBJECT "$SMTP_SUBJECT"
+  _debug SMTP_CONTENT "$SMTP_CONTENT"
+
+  # Send the message:
+  case "$(basename "$SMTP_BIN")" in
+  curl) _smtp_send=_smtp_send_curl ;;
+  py*) _smtp_send=_smtp_send_python ;;
+  *)
+    _err "Can't figure out how to invoke '$SMTP_BIN'."
+    _err "Check your SMTP_BIN setting."
+    return 1
+    ;;
+  esac
+
+  if ! smtp_output="$($_smtp_send)"; then
+    _err "Error sending message with $SMTP_BIN."
+    if [ -n "$smtp_output" ]; then
+      _err "$smtp_output"
+    fi
+    return 1
+  fi
+
+  return 0
+}
+
+# Strip CR and NL from text to prevent MIME header injection
+# text
+_clean_email_header() {
+  printf "%s" "$(echo "$1" | tr -d "\r\n")"
+}
+
+# Simple check for display name in an email address (< > or ")
+# email
+_email_has_display_name() {
+  _email="$1"
+  expr "$_email" : '^.*[<>"]' >/dev/null
+}
+
+##
+## curl smtp sending
+##
+
+# Send the message via curl using SMTP_* variables
+_smtp_send_curl() {
+  # Build curl args in $@
+  case "$SMTP_SECURE" in
+  none)
+    set -- --url "smtp://${SMTP_HOST}:${SMTP_PORT}"
+    ;;
+  ssl)
+    set -- --url "smtps://${SMTP_HOST}:${SMTP_PORT}"
+    ;;
+  tls)
+    set -- --url "smtp://${SMTP_HOST}:${SMTP_PORT}" --ssl-reqd
+    ;;
+  *)
+    # This will only occur if someone adds a new SMTP_SECURE option above
+    # without updating this code for it.
+    _err "Unhandled SMTP_SECURE='$SMTP_SECURE' in _smtp_send_curl"
+    _err "Please re-run with --debug and report a bug."
+    return 1
+    ;;
+  esac
+
+  set -- "$@" \
+    --upload-file - \
+    --mail-from "$SMTP_FROM" \
+    --max-time "$SMTP_TIMEOUT"
+
+  # Burst comma-separated $SMTP_TO into individual --mail-rcpt args.
+  _to="${SMTP_TO},"
+  while [ -n "$_to" ]; do
+    _rcpt="${_to%%,*}"
+    _to="${_to#*,}"
+    set -- "$@" --mail-rcpt "$_rcpt"
+  done
+
+  _smtp_login="${SMTP_USERNAME}:${SMTP_PASSWORD}"
+  if [ "$_smtp_login" != ":" ]; then
+    set -- "$@" --user "$_smtp_login"
+  fi
+
+  if [ "$SMTP_SHOW_TRANSCRIPT" = "True" ]; then
+    set -- "$@" --verbose
+  else
+    set -- "$@" --silent --show-error
+  fi
+
+  raw_message="$(_smtp_raw_message)"
+
+  _debug2 "curl command:" "$SMTP_BIN" "$*"
+  _debug2 "raw_message:\n$raw_message"
+
+  echo "$raw_message" | "$SMTP_BIN" "$@"
+}
+
+# Output an RFC-822 / RFC-5322 email message using SMTP_* variables.
+# (This assumes variables have already been cleaned for use in email headers.)
+_smtp_raw_message() {
+  echo "From: $SMTP_FROM"
+  echo "To: $SMTP_TO"
+  echo "Subject: $(_mime_encoded_word "$SMTP_SUBJECT")"
+  echo "Date: $(_rfc2822_date)"
+  echo "Content-Type: text/plain; charset=utf-8"
+  echo "X-Mailer: $SMTP_X_MAILER"
+  echo
+  echo "$SMTP_CONTENT"
+}
+
+# Convert text to RFC-2047 MIME "encoded word" format if it contains non-ASCII chars
+# text
+_mime_encoded_word() {
+  _text="$1"
+  # (regex character ranges like [a-z] can be locale-dependent; enumerate ASCII chars to avoid that)
+  _ascii='] $`"'"[!#%&'()*+,./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ~^_abcdefghijklmnopqrstuvwxyz{|}~-"
+  if expr "$_text" : "^.*[^$_ascii]" >/dev/null; then
+    # At least one non-ASCII char; convert entire thing to encoded word
+    printf "%s" "=?UTF-8?B?$(printf "%s" "$_text" | _base64)?="
+  else
+    # Just printable ASCII, no conversion needed
+    printf "%s" "$_text"
+  fi
+}
+
+# Output current date in RFC-2822 Section 3.3 format as required in email headers
+# (e.g., "Mon, 15 Feb 2021 14:22:01 -0800")
+_rfc2822_date() {
+  # Notes:
+  #   - this is deliberately not UTC, because it "SHOULD express local time" per spec
+  #   - the spec requires weekday and month in the C locale (English), not localized
+  #   - this date format specifier has been tested on Linux, Mac, Solaris and FreeBSD
+  _old_lc_time="$LC_TIME"
+  LC_TIME=C
+  date +'%a, %-d %b %Y %H:%M:%S %z'
+  LC_TIME="$_old_lc_time"
+}
+
+##
+## Python smtp sending
+##
+
+# Send the message via Python using SMTP_* variables
+_smtp_send_python() {
+  _debug "Python version" "$("$SMTP_BIN" --version 2>&1)"
+
+  # language=Python
+  "$SMTP_BIN" <<PYTHON
+# This code is meant to work with either Python 2.7.x or Python 3.4+.
+try:
+    try:
+        from email.message import EmailMessage
+        from email.policy import default as email_policy_default
+    except ImportError:
+        # Python 2 (or < 3.3)
+        from email.mime.text import MIMEText as EmailMessage
+        email_policy_default = None
+    from email.utils import formatdate as rfc2822_date
+    from smtplib import SMTP, SMTP_SSL, SMTPException
+    from socket import error as SocketError
+except ImportError as err:
+    print("A required Python standard package is missing. This system may have"
+          " a reduced version of Python unsuitable for sending mail: %s" % err)
+    exit(1)
+
+show_transcript = """$SMTP_SHOW_TRANSCRIPT""" == "True"
+
+smtp_host = """$SMTP_HOST"""
+smtp_port = int("""$SMTP_PORT""")
+smtp_secure = """$SMTP_SECURE"""
+username = """$SMTP_USERNAME"""
+password = """$SMTP_PASSWORD"""
+timeout=int("""$SMTP_TIMEOUT""")  # seconds
+x_mailer="""$SMTP_X_MAILER"""
+
+from_email="""$SMTP_FROM"""
+to_emails="""$SMTP_TO"""  # can be comma-separated
+subject="""$SMTP_SUBJECT"""
+content="""$SMTP_CONTENT"""
+
+try:
+    msg = EmailMessage(policy=email_policy_default)
+    msg.set_content(content)
+except (AttributeError, TypeError):
+    # Python 2 MIMEText
+    msg = EmailMessage(content)
+msg["Subject"] = subject
+msg["From"] = from_email
+msg["To"] = to_emails
+msg["Date"] = rfc2822_date(localtime=True)
+msg["X-Mailer"] = x_mailer
+
+smtp = None
+try:
+    if smtp_secure == "ssl":
+        smtp = SMTP_SSL(smtp_host, smtp_port, timeout=timeout)
+    else:
+        smtp = SMTP(smtp_host, smtp_port, timeout=timeout)
+    smtp.set_debuglevel(show_transcript)
+    if smtp_secure == "tls":
+        smtp.starttls()
+    if username or password:
+        smtp.login(username, password)
+    smtp.sendmail(msg["From"], msg["To"].split(","), msg.as_string())
+
+except SMTPException as err:
+    # Output just the error (skip the Python stack trace) for SMTP errors
+    print("Error sending: %r" % err)
+    exit(1)
+
+except SocketError as err:
+    print("Error connecting to %s:%d: %r" % (smtp_host, smtp_port, err))
+    exit(1)
+
+finally:
+    if smtp is not None:
+        smtp.quit()
+PYTHON
+}
+
+##
+## Conf helpers
+##
+
+#_readaccountconf_mutable_default name default_value
+# Given a name like MY_CONF:
+#   - if MY_CONF is set and non-empty, output $MY_CONF
+#   - if MY_CONF is set _empty_, output $default_value
+#     (lets user `export MY_CONF=` to clear previous saved value
+#     and return to default, without user having to know default)
+#   - otherwise if _readaccountconf_mutable MY_CONF is non-empty, return that
+#     (value of SAVED_MY_CONF from account.conf)
+#   - otherwise output $default_value
+_readaccountconf_mutable_default() {
+  _name="$1"
+  _default_value="$2"
+
+  eval "_value=\"\$$_name\""
+  eval "_name_is_set=\"\${${_name}+true}\""
+  # ($_name_is_set is "true" if $$_name is set to anything, including empty)
+  if [ -z "${_value}" ] && [ "${_name_is_set:-}" != "true" ]; then
+    _value="$(_readaccountconf_mutable "$_name")"
+  fi
+  if [ -z "${_value}" ]; then
+    _value="$_default_value"
+  fi
+  printf "%s" "$_value"
+}
+
+#_saveaccountconf_mutable_default name value default_value base64encode
+# Like _saveaccountconf_mutable, but if value is default_value
+# then _clearaccountconf_mutable instead
+_saveaccountconf_mutable_default() {
+  _name="$1"
+  _value="$2"
+  _default_value="$3"
+  _base64encode="$4"
+
+  if [ "$_value" != "$_default_value" ]; then
+    _saveaccountconf_mutable "$_name" "$_value" "$_base64encode"
+  else
+    _clearaccountconf_mutable "$_name"
+  fi
 }