Browse Source

Merge branch 'dev' of https://github.com/Neilpang/acme.sh into dev

David Kerr 6 years ago
parent
commit
7368a790a3

+ 1 - 0
Dockerfile

@@ -48,6 +48,7 @@ RUN for verb in help \
   createCSR \
   deactivate \
   deactivate-account \
+  set-notify \
   ; do \
     printf -- "%b" "#!/usr/bin/env sh\n/root/.acme.sh/acme.sh --${verb} --config-home /acme.sh \"\$@\"" >/usr/local/bin/--${verb} && chmod +x /usr/local/bin/--${verb} \
   ; done

+ 27 - 21
README.md

@@ -13,6 +13,7 @@
 - DOES NOT require `root/sudoer` access.
 - Docker friendly
 - IPv6 support
+- Cron job notifications for renewal or error etc.
 
 It's probably the `easiest & smartest` shell script to automatically issue & renew the free certificates from Let's Encrypt.
 
@@ -45,25 +46,25 @@ Twitter: [@neilpangxa](https://twitter.com/neilpangxa)
 
 | NO | Status| Platform|
 |----|-------|---------|
-|1|[![](https://cdn.rawgit.com/Neilpang/acmetest/master/status/ubuntu-latest.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)| Ubuntu
-|2|[![](https://cdn.rawgit.com/Neilpang/acmetest/master/status/debian-latest.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)| Debian
-|3|[![](https://cdn.rawgit.com/Neilpang/acmetest/master/status/centos-latest.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)|CentOS
-|4|[![](https://cdn.rawgit.com/Neilpang/acmetest/master/status/windows-cygwin.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)|Windows (cygwin with curl, openssl and crontab included)
-|5|[![](https://cdn.rawgit.com/Neilpang/acmetest/master/status/freebsd.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)|FreeBSD
-|6|[![](https://cdn.rawgit.com/Neilpang/acmetest/master/status/pfsense.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)|pfsense
-|7|[![](https://cdn.rawgit.com/Neilpang/acmetest/master/status/opensuse-latest.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)|openSUSE
-|8|[![](https://cdn.rawgit.com/Neilpang/acmetest/master/status/alpine-latest.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)|Alpine Linux (with curl)
-|9|[![](https://cdn.rawgit.com/Neilpang/acmetest/master/status/base-archlinux.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)|Archlinux
-|10|[![](https://cdn.rawgit.com/Neilpang/acmetest/master/status/fedora-latest.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)|fedora
-|11|[![](https://cdn.rawgit.com/Neilpang/acmetest/master/status/kalilinux-kali-linux-docker.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)|Kali Linux
-|12|[![](https://cdn.rawgit.com/Neilpang/acmetest/master/status/oraclelinux-latest.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)|Oracle Linux
-|13|[![](https://cdn.rawgit.com/Neilpang/acmetest/master/status/proxmox.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)| Proxmox https://pve.proxmox.com/wiki/HTTPSCertificateConfiguration#Let.27s_Encrypt_using_acme.sh
+|1|[![](https://neilpang.github.io/acmetest/status/ubuntu-latest.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)| Ubuntu
+|2|[![](https://neilpang.github.io/acmetest/status/debian-latest.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)| Debian
+|3|[![](https://neilpang.github.io/acmetest/status/centos-latest.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)|CentOS
+|4|[![](https://neilpang.github.io/acmetest/status/windows-cygwin.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)|Windows (cygwin with curl, openssl and crontab included)
+|5|[![](https://neilpang.github.io/acmetest/status/freebsd.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)|FreeBSD
+|6|[![](https://neilpang.github.io/acmetest/status/pfsense.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)|pfsense
+|7|[![](https://neilpang.github.io/acmetest/status/opensuse-latest.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)|openSUSE
+|8|[![](https://neilpang.github.io/acmetest/status/alpine-latest.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)|Alpine Linux (with curl)
+|9|[![](https://neilpang.github.io/acmetest/status/base-archlinux.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)|Archlinux
+|10|[![](https://neilpang.github.io/acmetest/status/fedora-latest.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)|fedora
+|11|[![](https://neilpang.github.io/acmetest/status/kalilinux-kali-linux-docker.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)|Kali Linux
+|12|[![](https://neilpang.github.io/acmetest/status/oraclelinux-latest.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)|Oracle Linux
+|13|[![](https://neilpang.github.io/acmetest/status/proxmox.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)| Proxmox https://pve.proxmox.com/wiki/HTTPSCertificateConfiguration#Let.27s_Encrypt_using_acme.sh
 |14|-----| Cloud Linux  https://github.com/Neilpang/le/issues/111
-|15|[![](https://cdn.rawgit.com/Neilpang/acmetest/master/status/openbsd.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)|OpenBSD
-|16|[![](https://cdn.rawgit.com/Neilpang/acmetest/master/status/mageia.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)|Mageia
+|15|[![](https://neilpang.github.io/acmetest/status/openbsd.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)|OpenBSD
+|16|[![](https://neilpang.github.io/acmetest/status/mageia.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)|Mageia
 |17|-----| OpenWRT: Tested and working. See [wiki page](https://github.com/Neilpang/acme.sh/wiki/How-to-run-on-OpenWRT)
-|18|[![](https://cdn.rawgit.com/Neilpang/acmetest/master/status/solaris.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)|SunOS/Solaris
-|19|[![](https://cdn.rawgit.com/Neilpang/acmetest/master/status/gentoo-stage3-amd64.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)|Gentoo Linux
+|18|[![](https://neilpang.github.io/acmetest/status/solaris.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)|SunOS/Solaris
+|19|[![](https://neilpang.github.io/acmetest/status/gentoo-stage3-amd64.svg)](https://github.com/Neilpang/letest#here-are-the-latest-status)|Gentoo Linux
 |20|[![Build Status](https://travis-ci.org/Neilpang/acme.sh.svg?branch=master)](https://travis-ci.org/Neilpang/acme.sh)|Mac OSX
 
 For all build statuses, check our [weekly build project](https://github.com/Neilpang/acmetest):
@@ -432,20 +433,25 @@ acme.sh --upgrade --auto-upgrade 0
 https://github.com/Neilpang/acme.sh/wiki/Issue-a-cert-from-existing-CSR
 
 
-# 16. Under the Hood
+# 16. Send notifications in cronjob
+
+https://github.com/Neilpang/acme.sh/wiki/notify
+
+
+# 17. Under the Hood
 
 Speak ACME language using shell, directly to "Let's Encrypt".
 
 TODO:
 
 
-# 17. Acknowledgments
+# 18. Acknowledgments
 
 1. Acme-tiny: https://github.com/diafygi/acme-tiny
 2. ACME protocol: https://github.com/ietf-wg-acme/acme
 
 
-# 18. License & Others
+# 19. License & Others
 
 License is GPLv3
 
@@ -454,7 +460,7 @@ Please Star and Fork me.
 [Issues](https://github.com/Neilpang/acme.sh/issues) and [pull requests](https://github.com/Neilpang/acme.sh/pulls) are welcome.
 
 
-# 19. Donate
+# 20. Donate
 Your donation makes **acme.sh** better:
 
 1. PayPal/Alipay(支付宝)/Wechat(微信): [https://donate.acme.sh/](https://donate.acme.sh/)

+ 331 - 32
acme.sh

@@ -1,6 +1,6 @@
 #!/usr/bin/env sh
 
-VER=2.8.1
+VER=2.8.2
 
 PROJECT_NAME="acme.sh"
 
@@ -14,7 +14,11 @@ _WINDOWS_SCHEDULER_NAME="$PROJECT_NAME.cron"
 
 _SCRIPT_="$0"
 
-_SUB_FOLDERS="dnsapi deploy"
+_SUB_FOLDER_NOTIFY="notify"
+_SUB_FOLDER_DNSAPI="dnsapi"
+_SUB_FOLDER_DEPLOY="deploy"
+
+_SUB_FOLDERS="$_SUB_FOLDER_DNSAPI $_SUB_FOLDER_DEPLOY $_SUB_FOLDER_NOTIFY"
 
 LETSENCRYPT_CA_V1="https://acme-v01.api.letsencrypt.org/directory"
 LETSENCRYPT_STAGING_CA_V1="https://acme-staging.api.letsencrypt.org/directory"
@@ -107,6 +111,18 @@ SYSLOG_LEVEL_DEFAULT=$SYSLOG_LEVEL_ERROR
 #none
 SYSLOG_LEVEL_NONE=0
 
+NOTIFY_LEVEL_DISABLE=0
+NOTIFY_LEVEL_ERROR=1
+NOTIFY_LEVEL_RENEW=2
+NOTIFY_LEVEL_SKIP=3
+
+NOTIFY_LEVEL_DEFAULT=$NOTIFY_LEVEL_RENEW
+
+NOTIFY_MODE_BULK=0
+NOTIFY_MODE_CERT=1
+
+NOTIFY_MODE_DEFAULT=$NOTIFY_MODE_BULK
+
 _DEBUG_WIKI="https://github.com/Neilpang/acme.sh/wiki/How-to-debug-acme.sh"
 
 _PREPARE_LINK="https://github.com/Neilpang/acme.sh/wiki/Install-preparations"
@@ -117,6 +133,8 @@ _DNS_ALIAS_WIKI="https://github.com/Neilpang/acme.sh/wiki/DNS-alias-mode"
 
 _DNS_MANUAL_WIKI="https://github.com/Neilpang/acme.sh/wiki/dns-manual-mode"
 
+_NOTIFY_WIKI="https://github.com/Neilpang/acme.sh/wiki/notify"
+
 _DNS_MANUAL_ERR="The dns manual mode can not renew automatically, you must issue it again manually. You'd better use the other modes instead."
 
 _DNS_MANUAL_WARN="It seems that you are using dns manual mode. please take care: $_DNS_MANUAL_ERR"
@@ -784,6 +802,13 @@ _url_encode() {
   done
 }
 
+_json_encode() {
+  _j_str="$(sed 's/"/\\"/g' | sed "s/\r/\\r/g")"
+  _debug3 "_json_encode"
+  _debug3 "_j_str" "$_j_str"
+  echo "$_j_str" | _hex_dump | _lower_case | sed 's/0a/5c 6e/g' | tr -d ' ' | _h2b | tr -d "\r\n"
+}
+
 #options file
 _sed_i() {
   options="$1"
@@ -1032,7 +1057,7 @@ _createkey() {
 _is_idn() {
   _is_idn_d="$1"
   _debug2 _is_idn_d "$_is_idn_d"
-  _idn_temp=$(printf "%s" "$_is_idn_d" | tr -d '0-9' | tr -d 'a-z' | tr -d 'A-Z' | tr -d '*.,-')
+  _idn_temp=$(printf "%s" "$_is_idn_d" | tr -d '0-9' | tr -d 'a-z' | tr -d 'A-Z' | tr -d '*.,-_')
   _debug2 _idn_temp "$_idn_temp"
   [ "$_idn_temp" ]
 }
@@ -1084,18 +1109,19 @@ _createcsr() {
   printf "[ req_distinguished_name ]\n[ req ]\ndistinguished_name = req_distinguished_name\nreq_extensions = v3_req\n[ v3_req ]\n\nkeyUsage = nonRepudiation, digitalSignature, keyEncipherment" >"$csrconf"
 
   if [ "$acmeValidationv1" ]; then
+    domainlist="$(_idn "$domainlist")"
     printf -- "\nsubjectAltName=DNS:$domainlist" >>"$csrconf"
   elif [ -z "$domainlist" ] || [ "$domainlist" = "$NO_VALUE" ]; then
     #single domain
     _info "Single domain" "$domain"
-    printf -- "\nsubjectAltName=DNS:$domain" >>"$csrconf"
+    printf -- "\nsubjectAltName=DNS:$(_idn "$domain")" >>"$csrconf"
   else
     domainlist="$(_idn "$domainlist")"
     _debug2 domainlist "$domainlist"
     if _contains "$domainlist" ","; then
-      alt="DNS:$domain,DNS:$(echo "$domainlist" | sed "s/,,/,/g" | sed "s/,/,DNS:/g")"
+      alt="DNS:$(_idn "$domain"),DNS:$(echo "$domainlist" | sed "s/,,/,/g" | sed "s/,/,DNS:/g")"
     else
-      alt="DNS:$domain,DNS:$domainlist"
+      alt="DNS:$(_idn "$domain"),DNS:$domainlist"
     fi
     #multi
     _info "Multi domain" "$alt"
@@ -1363,7 +1389,7 @@ createDomainKey() {
       _info "The domain key is here: $(__green $CERT_KEY_PATH)"
       return 0
     else
-      _err "Can not domain key"
+      _err "Can not create domain key"
       return 1
     fi
   else
@@ -3018,11 +3044,12 @@ _clearupdns() {
         _err "It seems that your api file doesn't define $rmcommand"
         return 1
       fi
-
+      _info "Removing txt: $txt for domain: $txtdomain"
       if ! $rmcommand "$txtdomain" "$txt"; then
         _err "Error removing txt for domain:$txtdomain"
         return 1
       fi
+      _info "Removed: Success"
     )
 
   done
@@ -3167,6 +3194,14 @@ _on_issue_err() {
     _err "See: $_DEBUG_WIKI"
   fi
 
+  if [ "$IN_CRON" ]; then
+    if [ "$NOTIFY_LEVEL" ] && [ $NOTIFY_LEVEL -ge $NOTIFY_LEVEL_ERROR ]; then
+      if [ "$NOTIFY_MODE" = "$NOTIFY_MODE_CERT" ]; then
+        _send_notify "Renew $_main_domain error" "There is an error." "$NOTIFY_HOOK" 1
+      fi
+    fi
+  fi
+
   #run the post hook
   if [ "$_chk_post_hook" ]; then
     _info "Run post hook:'$_chk_post_hook'"
@@ -3209,6 +3244,13 @@ _on_issue_success() {
   _chk_post_hook="$1"
   _chk_renew_hook="$2"
   _debug _on_issue_success
+  if [ "$IN_CRON" ]; then
+    if [ "$NOTIFY_LEVEL" ] && [ $NOTIFY_LEVEL -ge $NOTIFY_LEVEL_RENEW ]; then
+      if [ "$NOTIFY_MODE" = "$NOTIFY_MODE_CERT" ]; then
+        _send_notify "Renew $_main_domain success" "Good, the cert is renewed." "$NOTIFY_HOOK" 0
+      fi
+    fi
+  fi
   #run the post hook
   if [ "$_chk_post_hook" ]; then
     _info "Run post hook:'$_chk_post_hook'"
@@ -3237,11 +3279,6 @@ _on_issue_success() {
 
 }
 
-updateaccount() {
-  _initpath
-  _regAccount
-}
-
 registeraccount() {
   _reg_length="$1"
   _initpath
@@ -3339,6 +3376,61 @@ _regAccount() {
   _info "ACCOUNT_THUMBPRINT" "$ACCOUNT_THUMBPRINT"
 }
 
+#implement updateaccount
+updateaccount() {
+  _initpath
+
+  if [ ! -f "$ACCOUNT_KEY_PATH" ] && [ -f "$_OLD_ACCOUNT_KEY" ]; then
+    _info "mv $_OLD_ACCOUNT_KEY to $ACCOUNT_KEY_PATH"
+    mv "$_OLD_ACCOUNT_KEY" "$ACCOUNT_KEY_PATH"
+  fi
+
+  if [ ! -f "$ACCOUNT_JSON_PATH" ] && [ -f "$_OLD_ACCOUNT_JSON" ]; then
+    _info "mv $_OLD_ACCOUNT_JSON to $ACCOUNT_JSON_PATH"
+    mv "$_OLD_ACCOUNT_JSON" "$ACCOUNT_JSON_PATH"
+  fi
+
+  if [ ! -f "$ACCOUNT_KEY_PATH" ]; then
+    _err "Account key is not found at: $ACCOUNT_KEY_PATH"
+    return 1
+  fi
+
+  _accUri=$(_readcaconf "ACCOUNT_URL")
+  _debug _accUri "$_accUri"
+
+  if [ -z "$_accUri" ]; then
+    _err "The account url is empty, please run '--update-account' first to update the account info first,"
+    _err "Then try again."
+    return 1
+  fi
+
+  if ! _calcjwk "$ACCOUNT_KEY_PATH"; then
+    return 1
+  fi
+  _initAPI
+
+  if [ "$ACME_VERSION" = "2" ]; then
+    if [ "$ACCOUNT_EMAIL" ]; then
+      updjson='{"contact": ["mailto: '$ACCOUNT_EMAIL'"]}'
+    fi
+  else
+    # ACMEv1: Updates happen the same way a registration is done.
+    # https://tools.ietf.org/html/draft-ietf-acme-acme-01#section-6.3
+    _regAccount
+    return
+  fi
+
+  # this part handles ACMEv2 account updates.
+  _send_signed_request "$_accUri" "$updjson"
+
+  if [ "$code" = '200' ]; then
+    _info "account update success for $_accUri."
+  else
+    _info "Error. The account was not updated."
+    return 1
+  fi
+}
+
 #Implement deactivate account
 deactivateaccount() {
   _initpath
@@ -3416,9 +3508,9 @@ _findHook() {
     d_api="$_SCRIPT_HOME/$_hookcat/$_hookname"
   elif [ -f "$_SCRIPT_HOME/$_hookcat/$_hookname.sh" ]; then
     d_api="$_SCRIPT_HOME/$_hookcat/$_hookname.sh"
-  elif [ -f "$LE_WORKING_DIR/$_hookdomain/$_hookname" ]; then
+  elif [ "$_hookdomain" ] && [ -f "$LE_WORKING_DIR/$_hookdomain/$_hookname" ]; then
     d_api="$LE_WORKING_DIR/$_hookdomain/$_hookname"
-  elif [ -f "$LE_WORKING_DIR/$_hookdomain/$_hookname.sh" ]; then
+  elif [ "$_hookdomain" ] && [ -f "$LE_WORKING_DIR/$_hookdomain/$_hookname.sh" ]; then
     d_api="$LE_WORKING_DIR/$_hookdomain/$_hookname.sh"
   elif [ -f "$LE_WORKING_DIR/$_hookname" ]; then
     d_api="$LE_WORKING_DIR/$_hookname"
@@ -3557,7 +3649,9 @@ _check_dns_entries() {
     for entry in $dns_entries; do
       d=$(_getfield "$entry" 1)
       txtdomain=$(_getfield "$entry" 2)
+      txtdomain=$(_idn "$txtdomain")
       aliasDomain=$(_getfield "$entry" 3)
+      aliasDomain=$(_idn "$aliasDomain")
       txt=$(_getfield "$entry" 5)
       d_api=$(_getfield "$entry" 6)
       _debug "d" "$d"
@@ -3754,7 +3848,7 @@ issue() {
   if [ -z "$vlist" ]; then
     if [ "$ACME_VERSION" = "2" ]; then
       #make new order request
-      _identifiers="{\"type\":\"dns\",\"value\":\"$_main_domain\"}"
+      _identifiers="{\"type\":\"dns\",\"value\":\"$(_idn "$_main_domain")\"}"
       _w_index=1
       while true; do
         d="$(echo "$_alt_domains," | cut -d , -f "$_w_index")"
@@ -3763,7 +3857,7 @@ issue() {
         if [ -z "$d" ]; then
           break
         fi
-        _identifiers="$_identifiers,{\"type\":\"dns\",\"value\":\"$d\"}"
+        _identifiers="$_identifiers,{\"type\":\"dns\",\"value\":\"$(_idn "$d")\"}"
       done
       _debug2 _identifiers "$_identifiers"
       if ! _send_signed_request "$ACME_NEW_ORDER" "{\"identifiers\": [$_identifiers]}"; then
@@ -3851,7 +3945,7 @@ $_authorizations_map"
       fi
 
       if [ "$ACME_VERSION" = "2" ]; then
-        response="$(echo "$_authorizations_map" | grep "^$d," | sed "s/$d,//")"
+        response="$(echo "$_authorizations_map" | grep "^$(_idn "$d")," | sed "s/$d,//")"
         _debug2 "response" "$response"
         if [ -z "$response" ]; then
           _err "get to authz error."
@@ -3964,13 +4058,13 @@ $_authorizations_map"
         txt="$(printf "%s" "$keyauthorization" | _digest "sha256" | _url_replace)"
         _debug txt "$txt"
 
-        d_api="$(_findHook "$_dns_root_d" dnsapi "$_currentRoot")"
+        d_api="$(_findHook "$_dns_root_d" $_SUB_FOLDER_DNSAPI "$_currentRoot")"
         _debug d_api "$d_api"
 
         dns_entry="$dns_entry$dvsep$txt${dvsep}$d_api"
         _debug2 dns_entry "$dns_entry"
         if [ "$d_api" ]; then
-          _info "Found domain api file: $d_api"
+          _debug "Found domain api file: $d_api"
         else
           if [ "$_currentRoot" != "$W_DNS" ]; then
             _err "Can not find dns api hook for: $_currentRoot"
@@ -3995,11 +4089,12 @@ $_authorizations_map"
             _err "It seems that your api file is not correct, it must have a function named: $addcommand"
             return 1
           fi
-
+          _info "Adding txt value: $txt for domain:  $txtdomain"
           if ! $addcommand "$txtdomain" "$txt"; then
             _err "Error add txt for domain:$txtdomain"
             return 1
           fi
+          _info "The txt record is added: Success."
         )
 
         if [ "$?" != "0" ]; then
@@ -4529,7 +4624,7 @@ renew() {
   _info "$(__green "Renew: '$Le_Domain'")"
   if [ ! -f "$DOMAIN_CONF" ]; then
     _info "'$Le_Domain' is not a issued domain, skip."
-    return 0
+    return $RENEW_SKIP
   fi
 
   if [ "$Le_RenewalDays" ]; then
@@ -4569,12 +4664,21 @@ renew() {
   if [ -z "$FORCE" ] && [ "$Le_NextRenewTime" ] && [ "$(_time)" -lt "$Le_NextRenewTime" ]; then
     _info "Skip, Next renewal time is: $(__green "$Le_NextRenewTimeStr")"
     _info "Add '$(__red '--force')' to force to renew."
+
+    if [ "$IN_CRON" = "1" ]; then
+      if [ "$NOTIFY_LEVEL" ] && [ $NOTIFY_LEVEL -ge $NOTIFY_LEVEL_SKIP ]; then
+        if [ "$NOTIFY_MODE" = "$NOTIFY_MODE_CERT" ]; then
+          _send_notify "Renew $Le_Domain skipped" "Good, the cert next renewal time is $Le_NextRenewTimeStr." "$NOTIFY_HOOK" "$RENEW_SKIP"
+        fi
+      fi
+    fi
+
     return "$RENEW_SKIP"
   fi
 
   if [ "$IN_CRON" = "1" ] && [ -z "$Le_CertCreateTime" ]; then
     _info "Skip invalid cert for: $Le_Domain"
-    return 0
+    return $RENEW_SKIP
   fi
 
   IS_RENEW="1"
@@ -4604,7 +4708,9 @@ renewAll() {
   _stopRenewOnError="$1"
   _debug "_stopRenewOnError" "$_stopRenewOnError"
   _ret="0"
-
+  _success_msg=""
+  _error_msg=""
+  _skipped_msg=""
   for di in "${CERT_HOME}"/*.*/; do
     _debug di "$di"
     if ! [ -d "$di" ]; then
@@ -4625,15 +4731,49 @@ renewAll() {
     if [ "$rc" != "0" ]; then
       if [ "$rc" = "$RENEW_SKIP" ]; then
         _info "Skipped $d"
-      elif [ "$_stopRenewOnError" ]; then
-        _err "Error renew $d,  stop now."
-        return "$rc"
+        _skipped_msg="${_skipped_msg}    $d
+"
       else
-        _ret="$rc"
-        _err "Error renew $d."
+        _error_msg="${_error_msg}    $d
+"
+        if [ "$_stopRenewOnError" ]; then
+          _err "Error renew $d,  stop now."
+          _ret="$rc"
+          break
+        else
+          _ret="$rc"
+          _err "Error renew $d."
+        fi
       fi
+    else
+      _success_msg="${_success_msg}    $d
+"
     fi
   done
+
+  if [ "$IN_CRON" = "1" ]; then
+    if [ -z "$NOTIFY_MODE" ] || [ "$NOTIFY_MODE" = "$NOTIFY_MODE_BULK" ]; then
+      _msg_subject="Renew"
+      if [ "$_error_msg" ]; then
+        _msg_subject="${_msg_subject} Error"
+      fi
+      if [ "$_success_msg" ]; then
+        _msg_subject="${_msg_subject} Success"
+      fi
+      if [ "$_skipped_msg" ]; then
+        _msg_subject="${_msg_subject} Skipped"
+      fi
+      _msg_data="Error certs:
+${_error_msg}
+Success certs:
+${_success_msg}
+Skipped certs:
+$_skipped_msg
+"
+      _send_notify "$_msg_subject" "$_msg_data" "$NOTIFY_HOOK" 0
+    fi
+  fi
+
   return "$_ret"
 }
 
@@ -4782,7 +4922,7 @@ _deploy() {
   _hooks="$2"
 
   for _d_api in $(echo "$_hooks" | tr ',' " "); do
-    _deployApi="$(_findHook "$_d" deploy "$_d_api")"
+    _deployApi="$(_findHook "$_d" $_SUB_FOLDER_DEPLOY "$_d_api")"
     if [ -z "$_deployApi" ]; then
       _err "The deploy hook $_d_api is not found."
       return 1
@@ -5732,6 +5872,113 @@ version() {
   echo "v$VER"
 }
 
+# subject content hooks code
+_send_notify() {
+  _nsubject="$1"
+  _ncontent="$2"
+  _nhooks="$3"
+  _nerror="$4"
+
+  if [ "$NOTIFY_LEVEL" = "$NOTIFY_LEVEL_DISABLE" ]; then
+    _debug "The NOTIFY_LEVEL is $NOTIFY_LEVEL, disabled, just return."
+    return 0
+  fi
+
+  if [ -z "$_nhooks" ]; then
+    _debug "The NOTIFY_HOOK is empty, just return."
+    return 0
+  fi
+
+  _send_err=0
+  for _n_hook in $(echo "$_nhooks" | tr ',' " "); do
+    _n_hook_file="$(_findHook "" $_SUB_FOLDER_NOTIFY "$_n_hook")"
+    _info "Found $_n_hook_file"
+
+    if ! (
+      if ! . "$_n_hook_file"; then
+        _err "Load file $_n_hook_file error. Please check your api file and try again."
+        return 1
+      fi
+
+      d_command="${_n_hook}_send"
+      if ! _exists "$d_command"; then
+        _err "It seems that your api file is not correct, it must have a function named: $d_command"
+        return 1
+      fi
+
+      if ! $d_command "$_nsubject" "$_ncontent" "$_nerror"; then
+        _err "Error send message by $d_command"
+        return 1
+      fi
+
+      return 0
+    ); then
+      _err "Set $_n_hook_file error."
+      _send_err=1
+    else
+      _info "$_n_hook $(__green Success)"
+    fi
+  done
+  return $_send_err
+
+}
+
+# hook
+_set_notify_hook() {
+  _nhooks="$1"
+
+  _test_subject="Hello, this is notification from $PROJECT_NAME"
+  _test_content="If you receive this email, your notification works."
+
+  _send_notify "$_test_subject" "$_test_content" "$_nhooks" 0
+
+}
+
+#[hook] [level] [mode]
+setnotify() {
+  _nhook="$1"
+  _nlevel="$2"
+  _nmode="$3"
+
+  _initpath
+
+  if [ -z "$_nhook$_nlevel$_nmode" ]; then
+    _usage "Usage: $PROJECT_ENTRY --set-notify [--notify-hook mailgun] [--notify-level $NOTIFY_LEVEL_DEFAULT] [--notify-mode $NOTIFY_MODE_DEFAULT]"
+    _usage "$_NOTIFY_WIKI"
+    return 1
+  fi
+
+  if [ "$_nlevel" ]; then
+    _info "Set notify level to: $_nlevel"
+    export "NOTIFY_LEVEL=$_nlevel"
+    _saveaccountconf "NOTIFY_LEVEL" "$NOTIFY_LEVEL"
+  fi
+
+  if [ "$_nmode" ]; then
+    _info "Set notify mode to: $_nmode"
+    export "NOTIFY_MODE=$_nmode"
+    _saveaccountconf "NOTIFY_MODE" "$NOTIFY_MODE"
+  fi
+
+  if [ "$_nhook" ]; then
+    _info "Set notify hook to: $_nhook"
+    if [ "$_nhook" = "$NO_VALUE" ]; then
+      _info "Clear notify hook"
+      _clearaccountconf "NOTIFY_HOOK"
+    else
+      if _set_notify_hook "$_nhook"; then
+        export NOTIFY_HOOK="$_nhook"
+        _saveaccountconf "NOTIFY_HOOK" "$NOTIFY_HOOK"
+        return 0
+      else
+        _err "Can not set notify hook to: $_nhook"
+        return 1
+      fi
+    fi
+  fi
+
+}
+
 showhelp() {
   _initpath
   version
@@ -5764,6 +6011,8 @@ Commands:
   --create-domain-key      Create an domain private key, professional use.
   --createCSR, -ccsr       Create CSR , professional use.
   --deactivate             Deactivate the domain authz, professional use.
+  --set-notify             Set the cron notification hook, level or mode.
+
 
 Parameters:
   --domain, -d   domain.tld         Specifies a domain, used to issue, renew or revoke etc.
@@ -5832,7 +6081,18 @@ Parameters:
   --use-wget                        Force to use wget, if you have both curl and wget installed.
   --yes-I-know-dns-manual-mode-enough-go-ahead-please  Force to use dns manual mode: $_DNS_MANUAL_WIKI
   --branch, -b                      Only valid for '--upgrade' command, specifies the branch name to upgrade to.
-  "
+
+  --notify-level  0|1|2|3           Set the notification level:  Default value is $NOTIFY_LEVEL_DEFAULT.
+                                     0: disabled, no notification will be sent. 
+                                     1: send notification only when there is an error. No news is good news.
+                                     2: send notification when a cert is successfully renewed, or there is an error
+                                     3: send notification when a cert is skipped, renewdd, or error
+  --notify-mode   0|1               Set notification mode. Default value is $NOTIFY_MODE_DEFAULT.
+                                     0: Bulk mode. Send all the domain's notifications in one message(mail)
+                                     1: Cert mode. Send a message for every single cert.
+  --notify-hook   [hookname]        Set the notify hook
+
+"
 }
 
 # nocron noprofile
@@ -5966,6 +6226,9 @@ _process() {
   _syslog=""
   _use_wget=""
   _server=""
+  _notify_hook=""
+  _notify_level=""
+  _notify_mode=""
   while [ ${#} -gt 0 ]; do
     case "${1}" in
 
@@ -6052,6 +6315,9 @@ _process() {
       --deactivate-account)
         _CMD="deactivateaccount"
         ;;
+      --set-notify)
+        _CMD="setnotify"
+        ;;
       --domain | -d)
         _dvalue="$2"
 
@@ -6400,6 +6666,37 @@ _process() {
         export BRANCH="$2"
         shift
         ;;
+      --notify-hook)
+        _nhook="$2"
+        if _startswith "$_nhook" "-"; then
+          _err "'$_nhook' is not a hook name for '$1'"
+          return 1
+        fi
+        if [ "$_notify_hook" ]; then
+          _notify_hook="$_notify_hook,$_nhook"
+        else
+          _notify_hook="$_nhook"
+        fi
+        shift
+        ;;
+      --notify-level)
+        _nlevel="$2"
+        if _startswith "$_nlevel" "-"; then
+          _err "'$_nlevel' is not a integer for '$1'"
+          return 1
+        fi
+        _notify_level="$_nlevel"
+        shift
+        ;;
+      --notify-mode)
+        _nmode="$2"
+        if _startswith "$_nmode" "-"; then
+          _err "'$_nmode' is not a integer for '$1'"
+          return 1
+        fi
+        _notify_mode="$_nmode"
+        shift
+        ;;
       *)
         _err "Unknown parameter : $1"
         return 1
@@ -6517,7 +6814,9 @@ _process() {
     createCSR)
       createCSR "$_domain" "$_altdomains" "$_ecc"
       ;;
-
+    setnotify)
+      setnotify "$_notify_hook" "$_notify_level" "$_notify_mode"
+      ;;
     *)
       if [ "$_CMD" ]; then
         _err "Invalid command: $_CMD"

+ 2 - 2
deploy/gcore_cdn.sh

@@ -27,8 +27,8 @@ gcore_cdn_deploy() {
   _debug _cca "$_cca"
   _debug _cfullchain "$_cfullchain"
 
-  _fullchain=$(tr '\n\r' '@#' <"$_cfullchain" | sed 's/@/\\n/g;s/#/\\r/g')
-  _key=$(tr '\n\r' '@#' <"$_ckey" | sed 's/@/\\n/g;s/#/\\r/g')
+  _fullchain=$(tr '\r\n' '*#' <"$_cfullchain" | sed 's/*#/#/g;s/##/#/g;s/#/\\n/g')
+  _key=$(tr '\r\n' '*#' <"$_ckey" | sed 's/*#/#/g;s/#/\\n/g')
 
   _debug _fullchain "$_fullchain"
   _debug _key "$_key"

+ 249 - 35
deploy/haproxy.sh

@@ -1,8 +1,41 @@
 #!/usr/bin/env sh
 
-#Here is a script to deploy cert to haproxy server.
-
-#returns 0 means success, otherwise error.
+# Script for acme.sh to deploy certificates to haproxy
+#
+# The following variables can be exported:
+#
+# export DEPLOY_HAPROXY_PEM_NAME="${domain}.pem"
+#
+# Defines the name of the PEM file.
+# Defaults to "<domain>.pem"
+#
+# export DEPLOY_HAPROXY_PEM_PATH="/etc/haproxy"
+#
+# Defines location of PEM file for HAProxy.
+# Defaults to /etc/haproxy
+#
+# export DEPLOY_HAPROXY_RELOAD="systemctl reload haproxy"
+#
+# OPTIONAL: Reload command used post deploy
+# This defaults to be a no-op (ie "true").
+# It is strongly recommended to set this something that makes sense
+# for your distro.
+#
+# export DEPLOY_HAPROXY_ISSUER="no"
+#
+# OPTIONAL: Places CA file as "${DEPLOY_HAPROXY_PEM}.issuer"
+# Note: Required for OCSP stapling to work
+#
+# export DEPLOY_HAPROXY_BUNDLE="no"
+#
+# OPTIONAL: Deploy this certificate as part of a multi-cert bundle
+# This adds a suffix to the certificate based on the certificate type
+# eg RSA certificates will have .rsa as a suffix to the file name
+# HAProxy will load all certificates and provide one or the other
+# depending on client capabilities
+# Note: This functionality requires HAProxy was compiled against
+# a version of OpenSSL that supports this.
+#
 
 ########  Public functions #####################
 
@@ -14,45 +47,226 @@ haproxy_deploy() {
   _cca="$4"
   _cfullchain="$5"
 
-  _debug _cdomain "$_cdomain"
-  _debug _ckey "$_ckey"
-  _debug _ccert "$_ccert"
-  _debug _cca "$_cca"
-  _debug _cfullchain "$_cfullchain"
-
-  # handle reload preference
-  DEFAULT_HAPROXY_RELOAD="/usr/sbin/service haproxy restart"
-  if [ -z "${DEPLOY_HAPROXY_RELOAD}" ]; then
-    _reload="${DEFAULT_HAPROXY_RELOAD}"
-    _cleardomainconf DEPLOY_HAPROXY_RELOAD
-  else
-    _reload="${DEPLOY_HAPROXY_RELOAD}"
-    _savedomainconf DEPLOY_HAPROXY_RELOAD "$DEPLOY_HAPROXY_RELOAD"
+  # Some defaults
+  DEPLOY_HAPROXY_PEM_PATH_DEFAULT="/etc/haproxy"
+  DEPLOY_HAPROXY_PEM_NAME_DEFAULT="${_cdomain}.pem"
+  DEPLOY_HAPROXY_BUNDLE_DEFAULT="no"
+  DEPLOY_HAPROXY_ISSUER_DEFAULT="no"
+  DEPLOY_HAPROXY_RELOAD_DEFAULT="true"
+
+  if [ -f "${DOMAIN_CONF}" ]; then
+    # shellcheck disable=SC1090
+    . "${DOMAIN_CONF}"
+  fi
+
+  _debug _cdomain "${_cdomain}"
+  _debug _ckey "${_ckey}"
+  _debug _ccert "${_ccert}"
+  _debug _cca "${_cca}"
+  _debug _cfullchain "${_cfullchain}"
+
+  # PEM_PATH is optional. If not provided then assume "${DEPLOY_HAPROXY_PEM_PATH_DEFAULT}"
+  if [ -n "${DEPLOY_HAPROXY_PEM_PATH}" ]; then
+    Le_Deploy_haproxy_pem_path="${DEPLOY_HAPROXY_PEM_PATH}"
+    _savedomainconf Le_Deploy_haproxy_pem_path "${Le_Deploy_haproxy_pem_path}"
+  elif [ -z "${Le_Deploy_haproxy_pem_path}" ]; then
+    Le_Deploy_haproxy_pem_path="${DEPLOY_HAPROXY_PEM_PATH_DEFAULT}"
   fi
-  _savedomainconf DEPLOY_HAPROXY_PEM_PATH "$DEPLOY_HAPROXY_PEM_PATH"
 
-  # work out the path where the PEM file should go
-  _pem_path="${DEPLOY_HAPROXY_PEM_PATH}"
-  if [ -z "$_pem_path" ]; then
-    _err "Path to save PEM file not found. Please define DEPLOY_HAPROXY_PEM_PATH."
+  # Ensure PEM_PATH exists
+  if [ -d "${Le_Deploy_haproxy_pem_path}" ]; then
+    _debug "PEM_PATH ${Le_Deploy_haproxy_pem_path} exists"
+  else
+    _err "PEM_PATH ${Le_Deploy_haproxy_pem_path} does not exist"
     return 1
   fi
-  _pem_full_path="$_pem_path/$_cdomain.pem"
-  _info "Full path to PEM $_pem_full_path"
 
-  # combine the key and fullchain into a single pem and install
-  cat "$_cfullchain" "$_ckey" >"$_pem_full_path"
-  chmod 600 "$_pem_full_path"
-  _info "Certificate successfully deployed"
+  # PEM_NAME is optional. If not provided then assume "${DEPLOY_HAPROXY_PEM_NAME_DEFAULT}"
+  if [ -n "${DEPLOY_HAPROXY_PEM_NAME}" ]; then
+    Le_Deploy_haproxy_pem_name="${DEPLOY_HAPROXY_PEM_NAME}"
+    _savedomainconf Le_Deploy_haproxy_pem_name "${Le_Deploy_haproxy_pem_name}"
+  elif [ -z "${Le_Deploy_haproxy_pem_name}" ]; then
+    Le_Deploy_haproxy_pem_name="${DEPLOY_HAPROXY_PEM_NAME_DEFAULT}"
+  fi
+
+  # BUNDLE is optional. If not provided then assume "${DEPLOY_HAPROXY_BUNDLE_DEFAULT}"
+  if [ -n "${DEPLOY_HAPROXY_BUNDLE}" ]; then
+    Le_Deploy_haproxy_bundle="${DEPLOY_HAPROXY_BUNDLE}"
+    _savedomainconf Le_Deploy_haproxy_bundle "${Le_Deploy_haproxy_bundle}"
+  elif [ -z "${Le_Deploy_haproxy_bundle}" ]; then
+    Le_Deploy_haproxy_bundle="${DEPLOY_HAPROXY_BUNDLE_DEFAULT}"
+  fi
 
-  # restart HAProxy
-  _info "Run reload: $_reload"
-  if eval "$_reload"; then
-    _info "Reload success!"
-    return 0
+  # ISSUER is optional. If not provided then assume "${DEPLOY_HAPROXY_ISSUER_DEFAULT}"
+  if [ -n "${DEPLOY_HAPROXY_ISSUER}" ]; then
+    Le_Deploy_haproxy_issuer="${DEPLOY_HAPROXY_ISSUER}"
+    _savedomainconf Le_Deploy_haproxy_issuer "${Le_Deploy_haproxy_issuer}"
+  elif [ -z "${Le_Deploy_haproxy_issuer}" ]; then
+    Le_Deploy_haproxy_issuer="${DEPLOY_HAPROXY_ISSUER_DEFAULT}"
+  fi
+
+  # RELOAD is optional. If not provided then assume "${DEPLOY_HAPROXY_RELOAD_DEFAULT}"
+  if [ -n "${DEPLOY_HAPROXY_RELOAD}" ]; then
+    Le_Deploy_haproxy_reload="${DEPLOY_HAPROXY_RELOAD}"
+    _savedomainconf Le_Deploy_haproxy_reload "${Le_Deploy_haproxy_reload}"
+  elif [ -z "${Le_Deploy_haproxy_reload}" ]; then
+    Le_Deploy_haproxy_reload="${DEPLOY_HAPROXY_RELOAD_DEFAULT}"
+  fi
+
+  # Set the suffix depending if we are creating a bundle or not
+  if [ "${Le_Deploy_haproxy_bundle}" = "yes" ]; then
+    _info "Bundle creation requested"
+    # Initialise $Le_Keylength if its not already set
+    if [ -z "${Le_Keylength}" ]; then
+      Le_Keylength=""
+    fi
+    if _isEccKey "${Le_Keylength}"; then
+      _info "ECC key type detected"
+      _suffix=".ecdsa"
+    else
+      _info "RSA key type detected"
+      _suffix=".rsa"
+    fi
   else
-    _err "Reload error"
-    return 1
+    _suffix=""
+  fi
+  _debug _suffix "${_suffix}"
+
+  # Set variables for later
+  _pem="${Le_Deploy_haproxy_pem_path}/${Le_Deploy_haproxy_pem_name}${_suffix}"
+  _issuer="${_pem}.issuer"
+  _ocsp="${_pem}.ocsp"
+  _reload="${Le_Deploy_haproxy_reload}"
+
+  _info "Deploying PEM file"
+  # Create a temporary PEM file
+  _temppem="$(_mktemp)"
+  _debug _temppem "${_temppem}"
+  cat "${_ckey}" "${_ccert}" "${_cca}" >"${_temppem}"
+  _ret="$?"
+
+  # Check that we could create the temporary file
+  if [ "${_ret}" != "0" ]; then
+    _err "Error code ${_ret} returned during PEM file creation"
+    [ -f "${_temppem}" ] && rm -f "${_temppem}"
+    return ${_ret}
+  fi
+
+  # Move PEM file into place
+  _info "Moving new certificate into place"
+  _debug _pem "${_pem}"
+  cat "${_temppem}" >"${_pem}"
+  _ret=$?
+
+  # Clean up temp file
+  [ -f "${_temppem}" ] && rm -f "${_temppem}"
+
+  # Deal with any failure of moving PEM file into place
+  if [ "${_ret}" != "0" ]; then
+    _err "Error code ${_ret} returned while moving new certificate into place"
+    return ${_ret}
+  fi
+
+  # Update .issuer file if requested
+  if [ "${Le_Deploy_haproxy_issuer}" = "yes" ]; then
+    _info "Updating .issuer file"
+    _debug _issuer "${_issuer}"
+    cat "${_cca}" >"${_issuer}"
+    _ret="$?"
+
+    if [ "${_ret}" != "0" ]; then
+      _err "Error code ${_ret} returned while copying issuer/CA certificate into place"
+      return ${_ret}
+    fi
+  else
+    [ -f "${_issuer}" ] && _err "Issuer file update not requested but .issuer file exists"
+  fi
+
+  # Update .ocsp file if certificate was requested with --ocsp/--ocsp-must-staple option
+  if [ -z "${Le_OCSP_Staple}" ]; then
+    Le_OCSP_Staple="0"
+  fi
+  if [ "${Le_OCSP_Staple}" = "1" ]; then
+    _info "Updating OCSP stapling info"
+    _debug _ocsp "${_ocsp}"
+    _info "Extracting OCSP URL"
+    _ocsp_url=$(openssl x509 -noout -ocsp_uri -in "${_pem}")
+    _debug _ocsp_url "${_ocsp_url}"
+
+    # Only process OCSP if URL was present
+    if [ "${_ocsp_url}" != "" ]; then
+      # Extract the hostname from the OCSP URL
+      _info "Extracting OCSP URL"
+      _ocsp_host=$(echo "${_ocsp_url}" | cut -d/ -f3)
+      _debug _ocsp_host "${_ocsp_host}"
+
+      # Only process the certificate if we have a .issuer file
+      if [ -r "${_issuer}" ]; then
+        # Check if issuer cert is also a root CA cert
+        _subjectdn=$(openssl x509 -in "${_issuer}" -subject -noout | cut -d'/' -f2,3,4,5,6,7,8,9,10)
+        _debug _subjectdn "${_subjectdn}"
+        _issuerdn=$(openssl x509 -in "${_issuer}" -issuer -noout | cut -d'/' -f2,3,4,5,6,7,8,9,10)
+        _debug _issuerdn "${_issuerdn}"
+        _info "Requesting OCSP response"
+        # Request the OCSP response from the issuer and store it
+        if [ "${_subjectdn}" = "${_issuerdn}" ]; then
+          # If the issuer is a CA cert then our command line has "-CAfile" added
+          openssl ocsp \
+            -issuer "${_issuer}" \
+            -cert "${_pem}" \
+            -url "${_ocsp_url}" \
+            -header Host "${_ocsp_host}" \
+            -respout "${_ocsp}" \
+            -verify_other "${_issuer}" \
+            -no_nonce \
+            -CAfile "${_issuer}" \
+            | grep -q "${_pem}: good"
+          _ret=$?
+        else
+          # Issuer is not a root CA so no "-CAfile" option
+          openssl ocsp \
+            -issuer "${_issuer}" \
+            -cert "${_pem}" \
+            -url "${_ocsp_url}" \
+            -header Host "${_ocsp_host}" \
+            -respout "${_ocsp}" \
+            -verify_other "${_issuer}" \
+            -no_nonce \
+            | grep -q "${_pem}: good"
+          _ret=$?
+        fi
+      else
+        # Non fatal: No issuer file was present so no OCSP stapling file created
+        _err "OCSP stapling in use but no .issuer file was present"
+      fi
+    else
+      # Non fatal: No OCSP url was found int the certificate
+      _err "OCSP update requested but no OCSP URL was found in certificate"
+    fi
+
+    # Non fatal: Check return code of openssl command
+    if [ "${_ret}" != "0" ]; then
+      _err "Updating OCSP stapling failed with return code ${_ret}"
+    fi
+  else
+    # An OCSP file was already present but certificate did not have OCSP extension
+    if [ -f "${_ocsp}" ]; then
+      _err "OCSP was not requested but .ocsp file exists."
+      # Could remove the file at this step, although HAProxy just ignores it in this case
+      # rm -f "${_ocsp}" || _err "Problem removing stale .ocsp file"
+    fi
+  fi
+
+  # Reload HAProxy
+  _debug _reload "${_reload}"
+  eval "${_reload}"
+  _ret=$?
+  if [ "${_ret}" != "0" ]; then
+    _err "Error code ${_ret} during reload"
+    return ${_ret}
+  else
+    _info "Reload successful"
   fi
 
+  return 0
 }

+ 3 - 1
dnsapi/README.md

@@ -1,4 +1,6 @@
 # How to use DNS API
 DNS api usage:
 
-https://github.com/Neilpang/acme.sh/wiki/dnsapi
+
+https://github.com/Neilpang/acme.sh/wiki/dnsapi
+

+ 83 - 0
dnsapi/dns_acmeproxy.sh

@@ -0,0 +1,83 @@
+#!/usr/bin/env sh
+
+## Acmeproxy DNS provider to be used with acmeproxy (http://github.com/mdbraber/acmeproxy)
+## API integration by Maarten den Braber
+##
+## Report any bugs via https://github.com/mdbraber/acme.sh
+
+dns_acmeproxy_add() {
+  fulldomain="${1}"
+  txtvalue="${2}"
+  action="present"
+
+  _debug "Calling: _acmeproxy_request() '${fulldomain}' '${txtvalue}' '${action}'"
+  _acmeproxy_request "$fulldomain" "$txtvalue" "$action"
+}
+
+dns_acmeproxy_rm() {
+  fulldomain="${1}"
+  txtvalue="${2}"
+  action="cleanup"
+
+  _debug "Calling: _acmeproxy_request() '${fulldomain}' '${txtvalue}' '${action}'"
+  _acmeproxy_request "$fulldomain" "$txtvalue" "$action"
+}
+
+_acmeproxy_request() {
+
+  ## Nothing to see here, just some housekeeping
+  fulldomain=$1
+  txtvalue=$2
+  action=$3
+
+  _info "Using acmeproxy"
+  _debug fulldomain "$fulldomain"
+  _debug txtvalue "$txtvalue"
+
+  ACMEPROXY_ENDPOINT="${ACMEPROXY_ENDPOINT:-$(_readaccountconf_mutable ACMEPROXY_ENDPOINT)}"
+  ACMEPROXY_USERNAME="${ACMEPROXY_USERNAME:-$(_readaccountconf_mutable ACMEPROXY_USERNAME)}"
+  ACMEPROXY_PASSWORD="${ACMEPROXY_PASSWORD:-$(_readaccountconf_mutable ACMEPROXY_PASSWORD)}"
+
+  ## Check for the endpoint
+  if [ -z "$ACMEPROXY_ENDPOINT" ]; then
+    ACMEPROXY_ENDPOINT=""
+    _err "You didn't specify the endpoint"
+    _err "Please set them via 'export ACMEPROXY_ENDPOINT=https://ip:port' and try again."
+    return 1
+  fi
+
+  ## Save the credentials to the account file
+  _saveaccountconf_mutable ACMEPROXY_ENDPOINT "$ACMEPROXY_ENDPOINT"
+  _saveaccountconf_mutable ACMEPROXY_USERNAME "$ACMEPROXY_USERNAME"
+  _saveaccountconf_mutable ACMEPROXY_PASSWORD "$ACMEPROXY_PASSWORD"
+
+  if [ -z "$ACMEPROXY_USERNAME" ] || [ -z "$ACMEPROXY_PASSWORD" ]; then
+    _info "ACMEPROXY_USERNAME and/or ACMEPROXY_PASSWORD not set - using without client authentication! Make sure you're using server authentication (e.g. IP-based)"
+    export _H1="Accept: application/json"
+    export _H2="Content-Type: application/json"
+  else
+    ## Base64 encode the credentials
+    credentials=$(printf "%b" "$ACMEPROXY_USERNAME:$ACMEPROXY_PASSWORD" | _base64)
+
+    ## Construct the HTTP Authorization header
+    export _H1="Authorization: Basic $credentials"
+    export _H2="Accept: application/json"
+    export _H3="Content-Type: application/json"
+  fi
+
+  ## Add the challenge record to the acmeproxy grid member
+  response="$(_post "{\"fqdn\": \"$fulldomain.\", \"value\": \"$txtvalue\"}" "$ACMEPROXY_ENDPOINT/$action" "" "POST")"
+
+  ## Let's see if we get something intelligible back from the unit
+  if echo "$response" | grep "\"$txtvalue\"" >/dev/null; then
+    _info "Successfully updated the txt record"
+    return 0
+  else
+    _err "Error encountered during record addition"
+    _err "$response"
+    return 1
+  fi
+
+}
+
+####################  Private functions below ##################################

+ 1 - 1
dnsapi/dns_active24.sh

@@ -129,7 +129,7 @@ _active24_init() {
     return 1
   fi
 
-  _saveaccountconf_mutable ACTIVE24_Token "ACTIVE24_Token"
+  _saveaccountconf_mutable ACTIVE24_Token "$ACTIVE24_Token"
 
   _debug "First detect the root zone"
   if ! _get_root "$fulldomain"; then

+ 7 - 4
dnsapi/dns_cf.sh

@@ -58,7 +58,7 @@ dns_cf_add() {
   #  if [ "$count" = "0" ]; then
   _info "Adding record"
   if _cf_rest POST "zones/$_domain_id/dns_records" "{\"type\":\"TXT\",\"name\":\"$fulldomain\",\"content\":\"$txtvalue\",\"ttl\":120}"; then
-    if _contains "$response" "$fulldomain"; then
+    if _contains "$response" "$txtvalue"; then
       _info "Added, OK"
       return 0
     elif _contains "$response" "The record already exists"; then
@@ -161,7 +161,7 @@ _get_root() {
       return 1
     fi
 
-    if _contains "$response" "\"name\":\"$h\"" >/dev/null; then
+    if _contains "$response" "\"name\":\"$h\"" || _contains "$response" '"total_count":1'; then
       _domain_id=$(echo "$response" | _egrep_o "\[.\"id\":\"[^\"]*\"" | _head_n 1 | cut -d : -f 2 | tr -d \")
       if [ "$_domain_id" ]; then
         _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
@@ -182,8 +182,11 @@ _cf_rest() {
   data="$3"
   _debug "$ep"
 
-  export _H1="X-Auth-Email: $CF_Email"
-  export _H2="X-Auth-Key: $CF_Key"
+  email_trimmed=$(echo $CF_Email | tr -d '"')
+  key_trimmed=$(echo $CF_Key | tr -d '"')
+
+  export _H1="X-Auth-Email: $email_trimmed"
+  export _H2="X-Auth-Key: $key_trimmed"
   export _H3="Content-Type: application/json"
 
   if [ "$m" != "GET" ]; then

+ 6 - 2
dnsapi/dns_cx.sh

@@ -16,6 +16,8 @@ dns_cx_add() {
   fulldomain=$1
   txtvalue=$2
 
+  CX_Key="${CX_Key:-$(_readaccountconf_mutable CX_Key)}"
+  CX_Secret="${CX_Secret:-$(_readaccountconf_mutable CX_Secret)}"
   if [ -z "$CX_Key" ] || [ -z "$CX_Secret" ]; then
     CX_Key=""
     CX_Secret=""
@@ -27,8 +29,8 @@ dns_cx_add() {
   REST_API="$CX_Api"
 
   #save the api key and email to the account conf file.
-  _saveaccountconf CX_Key "$CX_Key"
-  _saveaccountconf CX_Secret "$CX_Secret"
+  _saveaccountconf_mutable CX_Key "$CX_Key"
+  _saveaccountconf_mutable CX_Secret "$CX_Secret"
 
   _debug "First detect the root zone"
   if ! _get_root "$fulldomain"; then
@@ -43,6 +45,8 @@ dns_cx_add() {
 dns_cx_rm() {
   fulldomain=$1
   txtvalue=$2
+  CX_Key="${CX_Key:-$(_readaccountconf_mutable CX_Key)}"
+  CX_Secret="${CX_Secret:-$(_readaccountconf_mutable CX_Secret)}"
   REST_API="$CX_Api"
   if _get_root "$fulldomain"; then
     record_id=""

+ 130 - 0
dnsapi/dns_ddnss.sh

@@ -0,0 +1,130 @@
+#!/usr/bin/env sh
+
+#Created by RaidenII, to use DuckDNS's API to add/remove text records
+#modified by helbgd @ 03/13/2018 to support ddnss.de
+#modified by mod242 @ 04/24/2018 to support different ddnss domains
+#Please note: the Wildcard Feature must be turned on for the Host record
+#and the checkbox for TXT needs to be enabled
+
+# Pass credentials before "acme.sh --issue --dns dns_ddnss ..."
+# --
+# export DDNSS_Token="aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
+# --
+#
+
+DDNSS_DNS_API="https://ddnss.de/upd.php"
+
+########  Public functions #####################
+
+#Usage: dns_ddnss_add _acme-challenge.domain.ddnss.de "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+dns_ddnss_add() {
+  fulldomain=$1
+  txtvalue=$2
+
+  DDNSS_Token="${DDNSS_Token:-$(_readaccountconf_mutable DDNSS_Token)}"
+  if [ -z "$DDNSS_Token" ]; then
+    _err "You must export variable: DDNSS_Token"
+    _err "The token for your DDNSS account is necessary."
+    _err "You can look it up in your DDNSS account."
+    return 1
+  fi
+
+  # Now save the credentials.
+  _saveaccountconf_mutable DDNSS_Token "$DDNSS_Token"
+
+  # Unfortunately, DDNSS does not seems to support lookup domain through API
+  # So I assume your credentials (which are your domain and token) are correct
+  # If something goes wrong, we will get a KO response from DDNSS
+
+  if ! _ddnss_get_domain; then
+    return 1
+  fi
+
+  # Now add the TXT record to DDNSS DNS
+  _info "Trying to add TXT record"
+  if _ddnss_rest GET "key=$DDNSS_Token&host=$_ddnss_domain&txtm=1&txt=$txtvalue"; then
+    if [ "$response" = "Updated 1 hostname." ]; then
+      _info "TXT record has been successfully added to your DDNSS domain."
+      _info "Note that all subdomains under this domain uses the same TXT record."
+      return 0
+    else
+      _err "Errors happened during adding the TXT record, response=$response"
+      return 1
+    fi
+  else
+    _err "Errors happened during adding the TXT record."
+    return 1
+  fi
+}
+
+#Usage: fulldomain txtvalue
+#Remove the txt record after validation.
+dns_ddnss_rm() {
+  fulldomain=$1
+  txtvalue=$2
+
+  DDNSS_Token="${DDNSS_Token:-$(_readaccountconf_mutable DDNSS_Token)}"
+  if [ -z "$DDNSS_Token" ]; then
+    _err "You must export variable: DDNSS_Token"
+    _err "The token for your DDNSS account is necessary."
+    _err "You can look it up in your DDNSS account."
+    return 1
+  fi
+
+  if ! _ddnss_get_domain; then
+    return 1
+  fi
+
+  # Now remove the TXT record from DDNS DNS
+  _info "Trying to remove TXT record"
+  if _ddnss_rest GET "key=$DDNSS_Token&host=$_ddnss_domain&txtm=1&txt=."; then
+    if [ "$response" = "Updated 1 hostname." ]; then
+      _info "TXT record has been successfully removed from your DDNSS domain."
+      return 0
+    else
+      _err "Errors happened during removing the TXT record, response=$response"
+      return 1
+    fi
+  else
+    _err "Errors happened during removing the TXT record."
+    return 1
+  fi
+}
+
+####################  Private functions below ##################################
+
+#fulldomain=_acme-challenge.domain.ddnss.de
+#returns
+# _ddnss_domain=domain
+_ddnss_get_domain() {
+
+  # We'll extract the domain/username from full domain
+  _ddnss_domain="$(echo "$fulldomain" | _lower_case | _egrep_o '[.][^.][^.]*[.](ddnss|dyn-ip24|dyndns|dyn|dyndns1|home-webserver|myhome-server|dynip)\..*' | cut -d . -f 2-)"
+
+  if [ -z "$_ddnss_domain" ]; then
+    _err "Error extracting the domain."
+    return 1
+  fi
+
+  return 0
+}
+
+#Usage: method URI
+_ddnss_rest() {
+  method=$1
+  param="$2"
+  _debug param "$param"
+  url="$DDNSS_DNS_API?$param"
+  _debug url "$url"
+
+  # DDNSS uses GET to update domain info
+  if [ "$method" = "GET" ]; then
+    response="$(_get "$url" | sed 's/<[a-zA-Z\/][^>]*>//g' | _tail_n 1)"
+  else
+    _err "Unsupported method"
+    return 1
+  fi
+
+  _debug2 response "$response"
+  return 0
+}

+ 176 - 0
dnsapi/dns_durabledns.sh

@@ -0,0 +1,176 @@
+#!/usr/bin/env sh
+
+#DD_API_User="xxxxx"
+#DD_API_Key="xxxxxx"
+
+_DD_BASE="https://durabledns.com/services/dns"
+
+########  Public functions #####################
+
+#Usage: add  _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+dns_durabledns_add() {
+  fulldomain=$1
+  txtvalue=$2
+
+  DD_API_User="${DD_API_User:-$(_readaccountconf_mutable DD_API_User)}"
+  DD_API_Key="${DD_API_Key:-$(_readaccountconf_mutable DD_API_Key)}"
+  if [ -z "$DD_API_User" ] || [ -z "$DD_API_Key" ]; then
+    DD_API_User=""
+    DD_API_Key=""
+    _err "You didn't specify a durabledns api user or key yet."
+    _err "You can get yours from here https://durabledns.com/dashboard/index.php"
+    return 1
+  fi
+
+  #save the api key and email to the account conf file.
+  _saveaccountconf_mutable DD_API_User "$DD_API_User"
+  _saveaccountconf_mutable DD_API_Key "$DD_API_Key"
+
+  _debug "First detect the root zone"
+  if ! _get_root "$fulldomain"; then
+    _err "invalid domain"
+    return 1
+  fi
+
+  _debug _sub_domain "$_sub_domain"
+  _debug _domain "$_domain"
+
+  _dd_soap createRecord string zonename "$_domain." string name "$_sub_domain" string type "TXT" string data "$txtvalue" int aux 0 int ttl 10 string ddns_enabled N
+  _contains "$response" "createRecordResponse"
+}
+
+dns_durabledns_rm() {
+  fulldomain=$1
+  txtvalue=$2
+
+  DD_API_User="${DD_API_User:-$(_readaccountconf_mutable DD_API_User)}"
+  DD_API_Key="${DD_API_Key:-$(_readaccountconf_mutable DD_API_Key)}"
+  if [ -z "$DD_API_User" ] || [ -z "$DD_API_Key" ]; then
+    DD_API_User=""
+    DD_API_Key=""
+    _err "You didn't specify a durabledns api user or key yet."
+    _err "You can get yours from here https://durabledns.com/dashboard/index.php"
+    return 1
+  fi
+
+  _debug "First detect the root zone"
+  if ! _get_root "$fulldomain"; then
+    _err "invalid domain"
+    return 1
+  fi
+  _debug _sub_domain "$_sub_domain"
+  _debug _domain "$_domain"
+
+  _debug "Find record id"
+  if ! _dd_soap listRecords string zonename "$_domain."; then
+    _err "can not listRecords"
+    return 1
+  fi
+
+  subtxt="$(echo "$txtvalue" | cut -c 1-30)"
+  record="$(echo "$response" | sed 's/<item\>/#<item>/g' | tr '#' '\n' | grep ">$subtxt")"
+  _debug record "$record"
+  if [ -z "$record" ]; then
+    _err "can not find record for txtvalue" "$txtvalue"
+    _err "$response"
+    return 1
+  fi
+
+  recordid="$(echo "$record" | _egrep_o '<id xsi:type="xsd:int">[0-9]*</id>' | cut -d '>' -f 2 | cut -d '<' -f 1)"
+  _debug recordid "$recordid"
+  if [ -z "$recordid" ]; then
+    _err "can not find record id"
+    return 1
+  fi
+
+  if ! _dd_soap deleteRecord string zonename "$_domain." int id "$recordid"; then
+    _err "delete error"
+    return 1
+  fi
+
+  _contains "$response" "Success"
+}
+
+#_acme-challenge.www.domain.com
+#returns
+# _sub_domain=_acme-challenge.www
+# _domain=domain.com
+_get_root() {
+  domain=$1
+  if ! _dd_soap "listZones"; then
+    return 1
+  fi
+
+  i=1
+  p=1
+  while true; do
+    h=$(printf "%s" "$domain" | cut -d . -f $i-100)
+    _debug h "$h"
+    if [ -z "$h" ]; then
+      #not valid
+      return 1
+    fi
+
+    if _contains "$response" ">$h.</origin>"; then
+      _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
+      _domain=$h
+      return 0
+    fi
+    p=$i
+    i=$(_math "$i" + 1)
+  done
+  return 1
+
+}
+
+#method
+_dd_soap() {
+  _method="$1"
+  shift
+  _urn="${_method}wsdl"
+  # put the parameters to xml
+  body="<tns:$_method>
+      <apiuser xsi:type=\"xsd:string\">$DD_API_User</apiuser>
+      <apikey xsi:type=\"xsd:string\">$DD_API_Key</apikey>
+    "
+  while [ "$1" ]; do
+    _t="$1"
+    shift
+    _k="$1"
+    shift
+    _v="$1"
+    shift
+    body="$body<$_k xsi:type=\"xsd:$_t\">$_v</$_k>"
+  done
+  body="$body</tns:$_method>"
+  _debug2 "SOAP request ${body}"
+
+  # build SOAP XML
+  _xml='<?xml version="1.0" encoding="utf-8"?>
+<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" 
+xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/" 
+xmlns:tns="urn:'$_urn'" 
+xmlns:types="urn:'$_urn'/encodedTypes" 
+xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
+xmlns:xsd="http://www.w3.org/2001/XMLSchema">
+  <soap:Body soap:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">'"$body"'</soap:Body>
+</soap:Envelope>'
+
+  _debug2 _xml "$_xml"
+  # set SOAP headers
+  _action="SOAPAction: \"urn:$_urn#$_method\""
+  _debug2 "_action" "$_action"
+  export _H1="$_action"
+  export _H2="Content-Type: text/xml; charset=utf-8"
+
+  _url="$_DD_BASE/$_method.php"
+  _debug "_url" "$_url"
+  if ! response="$(_post "${_xml}" "${_url}")"; then
+    _err "Error <$1>"
+    return 1
+  fi
+  _debug2 "response" "$response"
+  response="$(echo "$response" | tr -d "\r\n" | _egrep_o ":${_method}Response .*:${_method}Response><")"
+  _debug2 "response" "$response"
+  return 0
+}

+ 3 - 3
dnsapi/dns_gcloud.sh

@@ -134,14 +134,14 @@ _dns_gcloud_find_zone() {
   filter="$filter)"
   _debug filter "$filter"
 
-  # List domains and find the longest match (in case of some levels of delegation)
+  # List domains and find the zone with the deepest sub-domain (in case of some levels of delegation)
   if ! match=$(gcloud dns managed-zones list \
     --format="value(name, dnsName)" \
     --filter="$filter" \
     | while read -r dnsName name; do
-      printf "%s\t%s\t%s\n" "${#dnsName}" "$dnsName" "$name"
+      printf "%s\t%s\t%s\n" "$(echo "$name" | awk -F"." '{print NF-1}')" "$dnsName" "$name"
     done \
-    | sort -n -r | _head_n 1 | cut -f2,3 | grep '^.*'); then
+      | sort -n -r | _head_n 1 | cut -f2,3 | grep '^.*'); then
     _err "_dns_gcloud_find_zone: Can't find a matching managed zone! Perhaps wrong project or gcloud credentials?"
     return 1
   fi

+ 30 - 2
dnsapi/dns_hostingde.sh

@@ -53,6 +53,18 @@ _hostingde_parse() {
   fi
 }
 
+_hostingde_parse_no_strip_whitespace() {
+  find="${1}"
+  if [ "${2}" ]; then
+    notfind="${2}"
+  fi
+  if [ "${notfind}" ]; then
+    _egrep_o \""${find}\":.*" | grep -v "${notfind}" | cut -d ':' -f 2 | cut -d ',' -f 1
+  else
+    _egrep_o \""${find}\":.*" | cut -d ':' -f 2 | cut -d ',' -f 1
+  fi
+}
+
 _hostingde_getZoneConfig() {
   _info "Getting ZoneConfig"
   curZone="${fulldomain#*.}"
@@ -85,6 +97,22 @@ _hostingde_getZoneConfig() {
       zoneConfigDnsServerGroupId=$(echo "${curResult}" | _hostingde_parse "dnsServerGroupId")
       zoneConfigEmailAddress=$(echo "${curResult}" | _hostingde_parse "emailAddress")
       zoneConfigDnsSecMode=$(echo "${curResult}" | _hostingde_parse "dnsSecMode")
+      zoneConfigTemplateValues=$(echo "${curResult}" | _hostingde_parse_no_strip_whitespace "templateValues")
+
+      if [ "$zoneConfigTemplateValues" != "null" ]; then
+        _debug "Zone is tied to a template."
+        zoneConfigTemplateValuesTemplateId=$(echo "${curResult}" | _hostingde_parse "templateId")
+        zoneConfigTemplateValuesTemplateName=$(echo "${curResult}" | _hostingde_parse_no_strip_whitespace "templateName")
+        zoneConfigTemplateValuesTemplateReplacementsIPv4=$(echo "${curResult}" | _hostingde_parse "ipv4Replacement")
+        zoneConfigTemplateValuesTemplateReplacementsIPv6=$(echo "${curResult}" | _hostingde_parse "ipv6Replacement")
+        zoneConfigTemplateValuesTemplateReplacementsMailIPv4=$(echo "${curResult}" | _hostingde_parse "mailIpv4Replacement")
+        zoneConfigTemplateValuesTemplateReplacementsMailIPv6=$(echo "${curResult}" | _hostingde_parse "mailIpv6Replacement")
+        zoneConfigTemplateValuesTemplateTieToTemplate=$(echo "${curResult}" | _hostingde_parse "tieToTemplate")
+
+        zoneConfigTemplateValues="{\"templateId\":${zoneConfigTemplateValuesTemplateId},\"templateName\":${zoneConfigTemplateValuesTemplateName},\"templateReplacements\":{\"ipv4Replacement\":${zoneConfigTemplateValuesTemplateReplacementsIPv4},\"ipv6Replacement\":${zoneConfigTemplateValuesTemplateReplacementsIPv6},\"mailIpv4Replacement\":${zoneConfigTemplateValuesTemplateReplacementsMailIPv4},\"mailIpv6Replacement\":${zoneConfigTemplateValuesTemplateReplacementsMailIPv6}},\"tieToTemplate\":${zoneConfigTemplateValuesTemplateTieToTemplate}}"
+        _debug "Template values: '{$zoneConfigTemplateValues}'"
+      fi
+
       if [ "${zoneConfigType}" != "\"NATIVE\"" ]; then
         _err "Zone is not native"
         returnCode=1
@@ -122,7 +150,7 @@ _hostingde_addRecord() {
     _hostingde_getZoneStatus
     _debug "Result of zoneStatus: '${zoneStatus}'"
   done
-  curData="{\"authToken\":\"${HOSTINGDE_APIKEY}\",\"zoneConfig\":{\"id\":${zoneConfigId},\"name\":${zoneConfigName},\"type\":${zoneConfigType},\"dnsServerGroupId\":${zoneConfigDnsServerGroupId},\"dnsSecMode\":${zoneConfigDnsSecMode},\"emailAddress\":${zoneConfigEmailAddress},\"soaValues\":{\"expire\":${zoneConfigExpire},\"negativeTtl\":${zoneConfigNegativeTtl},\"refresh\":${zoneConfigRefresh},\"retry\":${zoneConfigRetry},\"ttl\":${zoneConfigTtl}}},\"recordsToAdd\":[{\"name\":\"${fulldomain}\",\"type\":\"TXT\",\"content\":\"\\\"${txtvalue}\\\"\",\"ttl\":3600}]}"
+  curData="{\"authToken\":\"${HOSTINGDE_APIKEY}\",\"zoneConfig\":{\"id\":${zoneConfigId},\"name\":${zoneConfigName},\"type\":${zoneConfigType},\"dnsServerGroupId\":${zoneConfigDnsServerGroupId},\"dnsSecMode\":${zoneConfigDnsSecMode},\"emailAddress\":${zoneConfigEmailAddress},\"soaValues\":{\"expire\":${zoneConfigExpire},\"negativeTtl\":${zoneConfigNegativeTtl},\"refresh\":${zoneConfigRefresh},\"retry\":${zoneConfigRetry},\"ttl\":${zoneConfigTtl}},\"templateValues\":${zoneConfigTemplateValues}},\"recordsToAdd\":[{\"name\":\"${fulldomain}\",\"type\":\"TXT\",\"content\":\"\\\"${txtvalue}\\\"\",\"ttl\":3600}]}"
   curResult="$(_post "${curData}" "${HOSTINGDE_ENDPOINT}/api/dns/v1/json/zoneUpdate")"
   _debug "Calling zoneUpdate: '${curData}' '${HOSTINGDE_ENDPOINT}/api/dns/v1/json/zoneUpdate'"
   _debug "Result of zoneUpdate: '$curResult'"
@@ -146,7 +174,7 @@ _hostingde_removeRecord() {
     _hostingde_getZoneStatus
     _debug "Result of zoneStatus: '$zoneStatus'"
   done
-  curData="{\"authToken\":\"${HOSTINGDE_APIKEY}\",\"zoneConfig\":{\"id\":${zoneConfigId},\"name\":${zoneConfigName},\"type\":${zoneConfigType},\"dnsServerGroupId\":${zoneConfigDnsServerGroupId},\"dnsSecMode\":${zoneConfigDnsSecMode},\"emailAddress\":${zoneConfigEmailAddress},\"soaValues\":{\"expire\":${zoneConfigExpire},\"negativeTtl\":${zoneConfigNegativeTtl},\"refresh\":${zoneConfigRefresh},\"retry\":${zoneConfigRetry},\"ttl\":${zoneConfigTtl}}},\"recordsToDelete\":[{\"name\":\"${fulldomain}\",\"type\":\"TXT\",\"content\":\"\\\"${txtvalue}\\\"\"}]}"
+  curData="{\"authToken\":\"${HOSTINGDE_APIKEY}\",\"zoneConfig\":{\"id\":${zoneConfigId},\"name\":${zoneConfigName},\"type\":${zoneConfigType},\"dnsServerGroupId\":${zoneConfigDnsServerGroupId},\"dnsSecMode\":${zoneConfigDnsSecMode},\"emailAddress\":${zoneConfigEmailAddress},\"soaValues\":{\"expire\":${zoneConfigExpire},\"negativeTtl\":${zoneConfigNegativeTtl},\"refresh\":${zoneConfigRefresh},\"retry\":${zoneConfigRetry},\"ttl\":${zoneConfigTtl}},\"templateValues\":${zoneConfigTemplateValues}},\"recordsToDelete\":[{\"name\":\"${fulldomain}\",\"type\":\"TXT\",\"content\":\"\\\"${txtvalue}\\\"\"}]}"
   curResult="$(_post "${curData}" "${HOSTINGDE_ENDPOINT}/api/dns/v1/json/zoneUpdate")"
   _debug "Calling zoneUpdate: '${curData}' '${HOSTINGDE_ENDPOINT}/api/dns/v1/json/zoneUpdate'"
   _debug "Result of zoneUpdate: '$curResult'"

+ 180 - 0
dnsapi/dns_internetbs.sh

@@ -0,0 +1,180 @@
+#!/usr/bin/env sh
+
+#This is the Internet.BS api wrapper for acme.sh
+#
+#Author: <alexey@nelexa.ru> Ne-Lexa
+#Report Bugs here: https://github.com/Ne-Lexa/acme.sh
+
+#INTERNETBS_API_KEY="sdfsdfsdfljlbjkljlkjsdfoiwje"
+#INTERNETBS_API_PASSWORD="sdfsdfsdfljlbjkljlkjsdfoiwje"
+
+INTERNETBS_API_URL="https://api.internet.bs"
+
+########  Public functions #####################
+
+#Usage: dns_myapi_add   _acme-challenge.www.domain.com   "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs"
+dns_internetbs_add() {
+  fulldomain=$1
+  txtvalue=$2
+
+  INTERNETBS_API_KEY="${INTERNETBS_API_KEY:-$(_readaccountconf_mutable INTERNETBS_API_KEY)}"
+  INTERNETBS_API_PASSWORD="${INTERNETBS_API_PASSWORD:-$(_readaccountconf_mutable INTERNETBS_API_PASSWORD)}"
+
+  if [ -z "$INTERNETBS_API_KEY" ] || [ -z "$INTERNETBS_API_PASSWORD" ]; then
+    INTERNETBS_API_KEY=""
+    INTERNETBS_API_PASSWORD=""
+    _err "You didn't specify the INTERNET.BS api key and password yet."
+    _err "Please create you key and try again."
+    return 1
+  fi
+
+  _saveaccountconf_mutable INTERNETBS_API_KEY "$INTERNETBS_API_KEY"
+  _saveaccountconf_mutable INTERNETBS_API_PASSWORD "$INTERNETBS_API_PASSWORD"
+
+  _debug "First detect the root zone"
+  if ! _get_root "$fulldomain"; then
+    _err "invalid domain"
+    return 1
+  fi
+
+  _debug _sub_domain "$_sub_domain"
+  _debug _domain "$_domain"
+
+  # https://testapi.internet.bs/Domain/DnsRecord/Add?ApiKey=testapi&Password=testpass&FullRecordName=w3.test-api-domain7.net&Type=CNAME&Value=www.internet.bs%&ResponseFormat=json
+  if _internetbs_rest POST "Domain/DnsRecord/Add" "FullRecordName=${_sub_domain}.${_domain}&Type=TXT&Value=${txtvalue}&ResponseFormat=json"; then
+    if ! _contains "$response" "\"status\":\"SUCCESS\""; then
+      _err "ERROR add TXT record"
+      _err "$response"
+      return 1
+    fi
+
+    _info "txt record add success."
+    return 0
+  fi
+
+  return 1
+}
+
+#Usage: fulldomain txtvalue
+#Remove the txt record after validation.
+dns_internetbs_rm() {
+  fulldomain=$1
+  txtvalue=$2
+
+  INTERNETBS_API_KEY="${INTERNETBS_API_KEY:-$(_readaccountconf_mutable INTERNETBS_API_KEY)}"
+  INTERNETBS_API_PASSWORD="${INTERNETBS_API_PASSWORD:-$(_readaccountconf_mutable INTERNETBS_API_PASSWORD)}"
+
+  if [ -z "$INTERNETBS_API_KEY" ] || [ -z "$INTERNETBS_API_PASSWORD" ]; then
+    INTERNETBS_API_KEY=""
+    INTERNETBS_API_PASSWORD=""
+    _err "You didn't specify the INTERNET.BS api key and password yet."
+    _err "Please create you key and try again."
+    return 1
+  fi
+
+  _debug "First detect the root zone"
+  if ! _get_root "$fulldomain"; then
+    _err "invalid domain"
+    return 1
+  fi
+
+  _debug _sub_domain "$_sub_domain"
+  _debug _domain "$_domain"
+
+  _debug "Getting txt records"
+  # https://testapi.internet.bs/Domain/DnsRecord/List?ApiKey=testapi&Password=testpass&Domain=test-api-domain7.net&FilterType=CNAME&ResponseFormat=json
+  _internetbs_rest POST "Domain/DnsRecord/List" "Domain=$_domain&FilterType=TXT&ResponseFormat=json"
+
+  if ! _contains "$response" "\"status\":\"SUCCESS\""; then
+    _err "ERROR list dns records"
+    _err "$response"
+    return 1
+  fi
+
+  if _contains "$response" "\name\":\"${_sub_domain}.${_domain}\""; then
+    _info "txt record find."
+
+    # https://testapi.internet.bs/Domain/DnsRecord/Remove?ApiKey=testapi&Password=testpass&FullRecordName=www.test-api-domain7.net&Type=cname&ResponseFormat=json
+    _internetbs_rest POST "Domain/DnsRecord/Remove" "FullRecordName=${_sub_domain}.${_domain}&Type=TXT&ResponseFormat=json"
+
+    if ! _contains "$response" "\"status\":\"SUCCESS\""; then
+      _err "ERROR remove dns record"
+      _err "$response"
+      return 1
+    fi
+
+    _info "txt record deleted success."
+    return 0
+  fi
+
+  return 1
+}
+
+####################  Private functions below ##################################
+#_acme-challenge.www.domain.com
+#returns
+# _sub_domain=_acme-challenge.www
+# _domain=domain.com
+# _domain_id=12345
+_get_root() {
+  domain=$1
+  i=2
+  p=1
+
+  # https://testapi.internet.bs/Domain/List?ApiKey=testapi&Password=testpass&CompactList=yes&ResponseFormat=json
+  if _internetbs_rest POST "Domain/List" "CompactList=yes&ResponseFormat=json"; then
+
+    if ! _contains "$response" "\"status\":\"SUCCESS\""; then
+      _err "ERROR fetch domain list"
+      _err "$response"
+      return 1
+    fi
+
+    while true; do
+      h=$(printf "%s" "$domain" | cut -d . -f ${i}-100)
+      _debug h "$h"
+      if [ -z "$h" ]; then
+        #not valid
+        return 1
+      fi
+
+      if _contains "$response" "\"$h\""; then
+        _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-${p})
+        _domain=${h}
+        return 0
+      fi
+
+      p=${i}
+      i=$(_math "$i" + 1)
+    done
+  fi
+  return 1
+}
+
+#Usage: method  URI  data
+_internetbs_rest() {
+  m="$1"
+  ep="$2"
+  data="$3"
+  url="${INTERNETBS_API_URL}/${ep}"
+
+  _debug url "$url"
+
+  apiKey="$(printf "%s" "${INTERNETBS_API_KEY}" | _url_encode)"
+  password="$(printf "%s" "${INTERNETBS_API_PASSWORD}" | _url_encode)"
+
+  if [ "$m" = "GET" ]; then
+    response="$(_get "${url}?ApiKey=${apiKey}&Password=${password}&${data}" | tr -d '\r')"
+  else
+    _debug2 data "$data"
+    response="$(_post "$data" "${url}?ApiKey=${apiKey}&Password=${password}" | tr -d '\r')"
+  fi
+
+  if [ "$?" != "0" ]; then
+    _err "error $ep"
+    return 1
+  fi
+
+  _debug2 response "$response"
+  return 0
+}

+ 38 - 22
dnsapi/dns_loopia.sh

@@ -4,8 +4,10 @@
 #LOOPIA_User="username"
 #
 #LOOPIA_Password="password"
+#
+#LOOPIA_Api="https://api.loopia.<TLD>/RPCSERV"
 
-LOOPIA_Api="https://api.loopia.se/RPCSERV"
+LOOPIA_Api_Default="https://api.loopia.se/RPCSERV"
 
 ########  Public functions #####################
 
@@ -14,19 +16,11 @@ dns_loopia_add() {
   fulldomain=$1
   txtvalue=$2
 
-  LOOPIA_User="${LOOPIA_User:-$(_readaccountconf_mutable LOOPIA_User)}"
-  LOOPIA_Password="${LOOPIA_Password:-$(_readaccountconf_mutable LOOPIA_Password)}"
-  if [ -z "$LOOPIA_User" ] || [ -z "$LOOPIA_Password" ]; then
-    LOOPIA_User=""
-    LOOPIA_Password=""
-    _err "You don't specify loopia user and password yet."
-    _err "Please create you key and try again."
+  if ! _loopia_load_config; then
     return 1
   fi
 
-  #save the api key and email to the account conf file.
-  _saveaccountconf_mutable LOOPIA_User "$LOOPIA_User"
-  _saveaccountconf_mutable LOOPIA_Password "$LOOPIA_Password"
+  _loopia_save_config
 
   _debug "First detect the root zone"
   if ! _get_root "$fulldomain"; then
@@ -47,19 +41,11 @@ dns_loopia_rm() {
   fulldomain=$1
   txtvalue=$2
 
-  LOOPIA_User="${LOOPIA_User:-$(_readaccountconf_mutable LOOPIA_User)}"
-  LOOPIA_Password="${LOOPIA_Password:-$(_readaccountconf_mutable LOOPIA_Password)}"
-  if [ -z "$LOOPIA_User" ] || [ -z "$LOOPIA_Password" ]; then
-    LOOPIA_User=""
-    LOOPIA_Password=""
-    _err "You don't specify LOOPIA user and password yet."
-    _err "Please create you key and try again."
+  if ! _loopia_load_config; then
     return 1
   fi
 
-  #save the api key and email to the account conf file.
-  _saveaccountconf_mutable LOOPIA_User "$LOOPIA_User"
-  _saveaccountconf_mutable LOOPIA_Password "$LOOPIA_Password"
+  _loopia_save_config
 
   _debug "First detect the root zone"
   if ! _get_root "$fulldomain"; then
@@ -84,7 +70,7 @@ dns_loopia_rm() {
         <value><string>%s</string></value>
       </param>
     </params>
-  </methodCall>' $LOOPIA_User $LOOPIA_Password "$_domain" "$_sub_domain")
+  </methodCall>' "$LOOPIA_User" "$LOOPIA_Password" "$_domain" "$_sub_domain")
 
   response="$(_post "$xml_content" "$LOOPIA_Api" "" "POST")"
 
@@ -96,6 +82,36 @@ dns_loopia_rm() {
 
 ####################  Private functions below ##################################
 
+_loopia_load_config() {
+  LOOPIA_Api="${LOOPIA_Api:-$(_readaccountconf_mutable LOOPIA_Api)}"
+  LOOPIA_User="${LOOPIA_User:-$(_readaccountconf_mutable LOOPIA_User)}"
+  LOOPIA_Password="${LOOPIA_Password:-$(_readaccountconf_mutable LOOPIA_Password)}"
+
+  if [ -z "$LOOPIA_Api" ]; then
+    LOOPIA_Api="$LOOPIA_Api_Default"
+  fi
+
+  if [ -z "$LOOPIA_User" ] || [ -z "$LOOPIA_Password" ]; then
+    LOOPIA_User=""
+    LOOPIA_Password=""
+
+    _err "A valid Loopia API user and password not provided."
+    _err "Please provide a valid API user and try again."
+
+    return 1
+  fi
+
+  return 0
+}
+
+_loopia_save_config() {
+  if [ "$LOOPIA_Api" != "$LOOPIA_Api_Default" ]; then
+    _saveaccountconf_mutable LOOPIA_Api "$LOOPIA_Api"
+  fi
+  _saveaccountconf_mutable LOOPIA_User "$LOOPIA_User"
+  _saveaccountconf_mutable LOOPIA_Password "$LOOPIA_Password"
+}
+
 _loopia_get_records() {
   domain=$1
   sub_domain=$2

+ 64 - 0
dnsapi/dns_nsd.sh

@@ -0,0 +1,64 @@
+#!/usr/bin/env sh
+
+#Nsd_ZoneFile="/etc/nsd/zones/example.com.zone"
+#Nsd_Command="sudo nsd-control reload"
+
+# args: fulldomain txtvalue
+dns_nsd_add() {
+  fulldomain=$1
+  txtvalue=$2
+  ttlvalue=300
+
+  Nsd_ZoneFile="${Nsd_ZoneFile:-$(_readdomainconf Nsd_ZoneFile)}"
+  Nsd_Command="${Nsd_Command:-$(_readdomainconf Nsd_Command)}"
+
+  # Arg checks
+  if [ -z "$Nsd_ZoneFile" ] || [ -z "$Nsd_Command" ]; then
+    Nsd_ZoneFile=""
+    Nsd_Command=""
+    _err "Specify ENV vars Nsd_ZoneFile and Nsd_Command"
+    return 1
+  fi
+
+  if [ ! -f "$Nsd_ZoneFile" ]; then
+    Nsd_ZoneFile=""
+    Nsd_Command=""
+    _err "No such file: $Nsd_ZoneFile"
+    return 1
+  fi
+
+  _savedomainconf Nsd_ZoneFile "$Nsd_ZoneFile"
+  _savedomainconf Nsd_Command "$Nsd_Command"
+
+  echo "$fulldomain. $ttlvalue IN TXT \"$txtvalue\"" >>"$Nsd_ZoneFile"
+  _info "Added TXT record for $fulldomain"
+  _debug "Running $Nsd_Command"
+  if eval "$Nsd_Command"; then
+    _info "Successfully updated the zone"
+    return 0
+  else
+    _err "Problem updating the zone"
+    return 1
+  fi
+}
+
+# args: fulldomain txtvalue
+dns_nsd_rm() {
+  fulldomain=$1
+  txtvalue=$2
+  ttlvalue=300
+
+  Nsd_ZoneFile="${Nsd_ZoneFile:-$(_readdomainconf Nsd_ZoneFile)}"
+  Nsd_Command="${Nsd_Command:-$(_readdomainconf Nsd_Command)}"
+
+  sed -i "/$fulldomain. $ttlvalue IN TXT \"$txtvalue\"/d" "$Nsd_ZoneFile"
+  _info "Removed TXT record for $fulldomain"
+  _debug "Running $Nsd_Command"
+  if eval "$Nsd_Command"; then
+    _info "Successfully reloaded NSD "
+    return 0
+  else
+    _err "Problem reloading NSD"
+    return 1
+  fi
+}

+ 139 - 0
dnsapi/dns_one.sh

@@ -0,0 +1,139 @@
+#!/usr/bin/env sh
+# -*- mode: sh; tab-width: 2; indent-tabs-mode: s; coding: utf-8 -*-
+
+# one.com ui wrapper for acme.sh
+# Author: github: @diseq
+# Created: 2019-02-17
+#
+#     export ONECOM_User="username"
+#     export ONECOM_Password="password"
+#
+# Usage:
+#     acme.sh --issue --dns dns_one -d example.com
+#
+#     only single domain supported atm
+
+dns_one_add() {
+  mysubdomain=$(printf -- "%s" "$1" | rev | cut -d"." -f3- | rev)
+  mydomain=$(printf -- "%s" "$1" | rev | cut -d"." -f1-2 | rev)
+  txtvalue=$2
+
+  # get credentials
+  ONECOM_User="${ONECOM_User:-$(_readaccountconf_mutable ONECOM_User)}"
+  ONECOM_Password="${ONECOM_Password:-$(_readaccountconf_mutable ONECOM_Password)}"
+  if [ -z "$ONECOM_User" ] || [ -z "$ONECOM_Password" ]; then
+    ONECOM_User=""
+    ONECOM_Password=""
+    _err "You didn't specify a one.com username and password yet."
+    _err "Please create the key and try again."
+    return 1
+  fi
+
+  #save the api key and email to the account conf file.
+  _saveaccountconf_mutable ONECOM_User "$ONECOM_User"
+  _saveaccountconf_mutable ONECOM_Password "$ONECOM_Password"
+
+  # Login with user and password
+  postdata="loginDomain=true"
+  postdata="$postdata&displayUsername=$ONECOM_User"
+  postdata="$postdata&username=$ONECOM_User"
+  postdata="$postdata&targetDomain=$mydomain"
+  postdata="$postdata&password1=$ONECOM_Password"
+  postdata="$postdata&loginTarget="
+  #_debug postdata "$postdata"
+
+  response="$(_post "$postdata" "https://www.one.com/admin/login.do" "" "POST" "application/x-www-form-urlencoded")"
+  #_debug response "$response"
+
+  JSESSIONID="$(grep "JSESSIONID" "$HTTP_HEADER" | grep "^[Ss]et-[Cc]ookie:" | _tail_n 1 | _egrep_o 'JSESSIONID=[^;]*;' | tr -d ';')"
+  _debug jsessionid "$JSESSIONID"
+
+  export _H1="Cookie: ${JSESSIONID}"
+
+  # get entries
+  response="$(_get "https://www.one.com/admin/api/domains/$mydomain/dns/custom_records")"
+  _debug response "$response"
+
+  CSRF_G_TOKEN="$(grep "CSRF_G_TOKEN=" "$HTTP_HEADER" | grep "^Set-Cookie:" | _tail_n 1 | _egrep_o 'CSRF_G_TOKEN=[^;]*;' | tr -d ';')"
+  export _H2="Cookie: ${CSRF_G_TOKEN}"
+
+  # Update the IP address for domain entry
+  postdata="{\"type\":\"dns_custom_records\",\"attributes\":{\"priority\":0,\"ttl\":600,\"type\":\"TXT\",\"prefix\":\"$mysubdomain\",\"content\":\"$txtvalue\"}}"
+  _debug postdata "$postdata"
+  response="$(_post "$postdata" "https://www.one.com/admin/api/domains/$mydomain/dns/custom_records" "" "POST" "application/json")"
+  response="$(echo "$response" | _normalizeJson)"
+  _debug response "$response"
+
+  id=$(echo "$response" | sed -n "s/{\"result\":{\"data\":{\"type\":\"dns_custom_records\",\"id\":\"\([^\"]*\)\",\"attributes\":{\"prefix\":\"$mysubdomain\",\"type\":\"TXT\",\"content\":\"$txtvalue\",\"priority\":0,\"ttl\":600}}},\"metadata\":null}/\1/p")
+
+  if [ -z "$id" ]; then
+    _err "Add txt record error."
+    return 1
+  else
+    _info "Added, OK ($id)"
+    return 0
+  fi
+
+}
+
+dns_one_rm() {
+  mysubdomain=$(printf -- "%s" "$1" | rev | cut -d"." -f3- | rev)
+  mydomain=$(printf -- "%s" "$1" | rev | cut -d"." -f1-2 | rev)
+  txtvalue=$2
+
+  # get credentials
+  ONECOM_User="${ONECOM_User:-$(_readaccountconf_mutable ONECOM_User)}"
+  ONECOM_Password="${ONECOM_Password:-$(_readaccountconf_mutable ONECOM_Password)}"
+  if [ -z "$ONECOM_User" ] || [ -z "$ONECOM_Password" ]; then
+    ONECOM_User=""
+    ONECOM_Password=""
+    _err "You didn't specify a one.com username and password yet."
+    _err "Please create the key and try again."
+    return 1
+  fi
+
+  # Login with user and password
+  postdata="loginDomain=true"
+  postdata="$postdata&displayUsername=$ONECOM_User"
+  postdata="$postdata&username=$ONECOM_User"
+  postdata="$postdata&targetDomain=$mydomain"
+  postdata="$postdata&password1=$ONECOM_Password"
+  postdata="$postdata&loginTarget="
+
+  response="$(_post "$postdata" "https://www.one.com/admin/login.do" "" "POST" "application/x-www-form-urlencoded")"
+  #_debug response "$response"
+
+  JSESSIONID="$(grep "JSESSIONID" "$HTTP_HEADER" | grep "^[Ss]et-[Cc]ookie:" | _tail_n 1 | _egrep_o 'JSESSIONID=[^;]*;' | tr -d ';')"
+  _debug jsessionid "$JSESSIONID"
+
+  export _H1="Cookie: ${JSESSIONID}"
+
+  # get entries
+  response="$(_get "https://www.one.com/admin/api/domains/$mydomain/dns/custom_records")"
+  response="$(echo "$response" | _normalizeJson)"
+  _debug response "$response"
+
+  CSRF_G_TOKEN="$(grep "CSRF_G_TOKEN=" "$HTTP_HEADER" | grep "^Set-Cookie:" | _tail_n 1 | _egrep_o 'CSRF_G_TOKEN=[^;]*;' | tr -d ';')"
+  export _H2="Cookie: ${CSRF_G_TOKEN}"
+
+  id=$(printf -- "%s" "$response" | sed -n "s/.*{\"type\":\"dns_custom_records\",\"id\":\"\([^\"]*\)\",\"attributes\":{\"prefix\":\"$mysubdomain\",\"type\":\"TXT\",\"content\":\"$txtvalue\",\"priority\":0,\"ttl\":600}.*/\1/p")
+
+  if [ -z "$id" ]; then
+    _err "Txt record not found."
+    return 1
+  fi
+
+  # delete entry
+  response="$(_post "$postdata" "https://www.one.com/admin/api/domains/$mydomain/dns/custom_records/$id" "" "DELETE" "application/json")"
+  response="$(echo "$response" | _normalizeJson)"
+  _debug response "$response"
+
+  if [ "$response" = '{"result":null,"metadata":null}' ]; then
+    _info "Removed, OK"
+    return 0
+  else
+    _err "Removing txt record error."
+    return 1
+  fi
+
+}

+ 261 - 0
dnsapi/dns_schlundtech.sh

@@ -0,0 +1,261 @@
+#!/usr/bin/env sh
+# -*- mode: sh; tab-width: 2; indent-tabs-mode: s; coding: utf-8 -*-
+
+# Schlundtech DNS API
+# Author: mod242
+# Created: 2019-40-29
+# Completly based on the autoDNS xml api wrapper by auerswald@gmail.com
+#
+#     export SCHLUNDTECH_USER="username"
+#     export SCHLUNDTECH_PASSWORD="password"
+#
+# Usage:
+#     acme.sh --issue --dns dns_schlundtech -d example.com
+
+SCHLUNDTECH_API="https://gateway.schlundtech.de"
+
+# Arguments:
+#   txtdomain
+#   txt
+dns_schlundtech_add() {
+  fulldomain="$1"
+  txtvalue="$2"
+
+  SCHLUNDTECH_USER="${SCHLUNDTECH_USER:-$(_readaccountconf_mutable SCHLUNDTECH_USER)}"
+  SCHLUNDTECH_PASSWORD="${SCHLUNDTECH_PASSWORD:-$(_readaccountconf_mutable SCHLUNDTECH_PASSWORD)}"
+
+  if [ -z "$SCHLUNDTECH_USER" ] || [ -z "$SCHLUNDTECH_PASSWORD" ]; then
+    _err "You didn't specify schlundtech user and password."
+    return 1
+  fi
+
+  _saveaccountconf_mutable SCHLUNDTECH_USER "$SCHLUNDTECH_USER"
+  _saveaccountconf_mutable SCHLUNDTECH_PASSWORD "$SCHLUNDTECH_PASSWORD"
+
+  _debug "First detect the root zone"
+
+  if ! _get_autodns_zone "$fulldomain"; then
+    _err "invalid domain"
+    return 1
+  fi
+
+  _debug _sub_domain "$_sub_domain"
+  _debug _zone "$_zone"
+  _debug _system_ns "$_system_ns"
+
+  _info "Adding TXT record"
+
+  autodns_response="$(_autodns_zone_update "$_zone" "$_sub_domain" "$txtvalue" "$_system_ns")"
+
+  if [ "$?" -eq "0" ]; then
+    _info "Added, OK"
+    return 0
+  fi
+
+  return 1
+}
+
+# Arguments:
+#   txtdomain
+#   txt
+dns_schlundtech_rm() {
+  fulldomain="$1"
+  txtvalue="$2"
+
+  SCHLUNDTECH_USER="${SCHLUNDTECH_USER:-$(_readaccountconf_mutable SCHLUNDTECH_USER)}"
+  SCHLUNDTECH_PASSWORD="${SCHLUNDTECH_PASSWORD:-$(_readaccountconf_mutable SCHLUNDTECH_PASSWORD)}"
+
+  if [ -z "$SCHLUNDTECH_USER" ] || [ -z "$SCHLUNDTECH_PASSWORD" ]; then
+    _err "You didn't specify schlundtech user and password."
+    return 1
+  fi
+
+  _debug "First detect the root zone"
+
+  if ! _get_autodns_zone "$fulldomain"; then
+    _err "zone not found"
+    return 1
+  fi
+
+  _debug _sub_domain "$_sub_domain"
+  _debug _zone "$_zone"
+  _debug _system_ns "$_system_ns"
+
+  _info "Delete TXT record"
+
+  autodns_response="$(_autodns_zone_cleanup "$_zone" "$_sub_domain" "$txtvalue" "$_system_ns")"
+
+  if [ "$?" -eq "0" ]; then
+    _info "Deleted, OK"
+    return 0
+  fi
+
+  return 1
+}
+
+####################  Private functions below ##################################
+
+# Arguments:
+#   fulldomain
+# Returns:
+#   _sub_domain=_acme-challenge.www
+#   _zone=domain.com
+#   _system_ns
+_get_autodns_zone() {
+  domain="$1"
+
+  i=2
+  p=1
+
+  while true; do
+    h=$(printf "%s" "$domain" | cut -d . -f $i-100)
+    _debug h "$h"
+
+    if [ -z "$h" ]; then
+      # not valid
+      return 1
+    fi
+
+    autodns_response="$(_autodns_zone_inquire "$h")"
+
+    if [ "$?" -ne "0" ]; then
+      _err "invalid domain"
+      return 1
+    fi
+
+    if _contains "$autodns_response" "<summary>1</summary>" >/dev/null; then
+      _zone="$(echo "$autodns_response" | _egrep_o '<name>[^<]*</name>' | cut -d '>' -f 2 | cut -d '<' -f 1)"
+      _system_ns="$(echo "$autodns_response" | _egrep_o '<system_ns>[^<]*</system_ns>' | cut -d '>' -f 2 | cut -d '<' -f 1)"
+      _sub_domain=$(printf "%s" "$domain" | cut -d . -f 1-$p)
+      return 0
+    fi
+
+    p=$i
+    i=$(_math "$i" + 1)
+  done
+
+  return 1
+}
+
+_build_request_auth_xml() {
+  printf "<auth>
+    <user>%s</user>
+    <password>%s</password>
+    <context>10</context>
+  </auth>" "$SCHLUNDTECH_USER" "$SCHLUNDTECH_PASSWORD"
+}
+
+# Arguments:
+#   zone
+_build_zone_inquire_xml() {
+  printf "<?xml version=\"1.0\" encoding=\"UTF-8\"?>
+  <request>
+    %s
+    <task>
+      <code>0205</code>
+      <view>
+        <children>1</children>
+        <limit>1</limit>
+      </view>
+      <where>
+        <key>name</key>
+        <operator>eq</operator>
+        <value>%s</value>
+      </where>
+    </task>
+  </request>" "$(_build_request_auth_xml)" "$1"
+}
+
+# Arguments:
+#   zone
+#   subdomain
+#   txtvalue
+#   system_ns
+_build_zone_update_xml() {
+  printf "<?xml version=\"1.0\" encoding=\"UTF-8\"?>
+  <request>
+    %s
+    <task>
+      <code>0202001</code>
+      <default>
+        <rr_add>
+          <name>%s</name>
+          <ttl>600</ttl>
+          <type>TXT</type>
+          <value>%s</value>
+        </rr_add>
+      </default>
+      <zone>
+        <name>%s</name>
+        <system_ns>%s</system_ns>
+      </zone>
+    </task>
+  </request>" "$(_build_request_auth_xml)" "$2" "$3" "$1" "$4"
+}
+
+# Arguments:
+#   zone
+_autodns_zone_inquire() {
+  request_data="$(_build_zone_inquire_xml "$1")"
+  autodns_response="$(_autodns_api_call "$request_data")"
+  ret="$?"
+
+  printf "%s" "$autodns_response"
+  return "$ret"
+}
+
+# Arguments:
+#   zone
+#   subdomain
+#   txtvalue
+#   system_ns
+_autodns_zone_update() {
+  request_data="$(_build_zone_update_xml "$1" "$2" "$3" "$4")"
+  autodns_response="$(_autodns_api_call "$request_data")"
+  ret="$?"
+
+  printf "%s" "$autodns_response"
+  return "$ret"
+}
+
+# Arguments:
+#   zone
+#   subdomain
+#   txtvalue
+#   system_ns
+_autodns_zone_cleanup() {
+  request_data="$(_build_zone_update_xml "$1" "$2" "$3" "$4")"
+  # replace 'rr_add>' with 'rr_rem>' in request_data
+  request_data="$(printf -- "%s" "$request_data" | sed 's/rr_add>/rr_rem>/g')"
+  autodns_response="$(_autodns_api_call "$request_data")"
+  ret="$?"
+
+  printf "%s" "$autodns_response"
+  return "$ret"
+}
+
+# Arguments:
+#   request_data
+_autodns_api_call() {
+  request_data="$1"
+
+  _debug request_data "$request_data"
+
+  autodns_response="$(_post "$request_data" "$SCHLUNDTECH_API")"
+  ret="$?"
+
+  _debug autodns_response "$autodns_response"
+
+  if [ "$ret" -ne "0" ]; then
+    _err "error"
+    return 1
+  fi
+
+  if _contains "$autodns_response" "<type>success</type>" >/dev/null; then
+    _info "success"
+    printf "%s" "$autodns_response"
+    return 0
+  fi
+
+  return 1
+}

+ 12 - 9
dnsapi/dns_yandex.sh

@@ -16,7 +16,7 @@ dns_yandex_add() {
   _PDD_credentials || return 1
   export _H1="PddToken: $PDD_Token"
 
-  _PDD_get_domain "$fulldomain"
+  _PDD_get_domain "$fulldomain" || return 1
   _debug "Found suitable domain in pdd: $curDomain"
   curData="domain=${curDomain}&type=TXT&subdomain=${curSubdomain}&ttl=360&content=${txtvalue}"
   curUri="https://pddimp.yandex.ru/api2/admin/dns/add"
@@ -30,16 +30,19 @@ dns_yandex_rm() {
   _debug "Calling: dns_yandex_rm() '${fulldomain}'"
   _PDD_credentials || return 1
   export _H1="PddToken: $PDD_Token"
-  record_id=$(pdd_get_record_id "${fulldomain}")
-  _debug "Result: $record_id"
 
-  _PDD_get_domain "$fulldomain"
+  _PDD_get_domain "$fulldomain" || return 1
   _debug "Found suitable domain in pdd: $curDomain"
 
-  curUri="https://pddimp.yandex.ru/api2/admin/dns/del"
-  curData="domain=${curDomain}&record_id=${record_id}"
-  curResult="$(_post "${curData}" "${curUri}")"
-  _debug "Result: $curResult"
+  record_id=$(pdd_get_record_id "${fulldomain}")
+  _debug "Result: $record_id"
+
+  for rec_i in $record_id; do
+    curUri="https://pddimp.yandex.ru/api2/admin/dns/del"
+    curData="domain=${curDomain}&record_id=${rec_i}"
+    curResult="$(_post "${curData}" "${curUri}")"
+    _debug "Result: $curResult"
+  done
 }
 
 ####################  Private functions below ##################################
@@ -54,7 +57,7 @@ _PDD_get_domain() {
     _debug2 "res1" "$res1"
     __found="$(echo "$res1" | sed -n -e 's#.* "found": \([^,]*\),.*#\1#p')"
     _debug "found: $__found results on page"
-    if [ "$__found" -lt 20 ]; then
+    if [ "0$__found" -lt 20 ]; then
       _debug "last page: $__page"
       __last=1
     fi

+ 120 - 0
notify/mail.sh

@@ -0,0 +1,120 @@
+#!/usr/bin/env sh
+
+#Support local mail app
+
+#MAIL_BIN="sendmail"
+#MAIL_FROM="yyyy@gmail.com"
+#MAIL_TO="yyyy@gmail.com"
+
+mail_send() {
+  _subject="$1"
+  _content="$2"
+  _statusCode="$3" #0: success, 1: error 2($RENEW_SKIP): skipped
+  _debug "_subject" "$_subject"
+  _debug "_content" "$_content"
+  _debug "_statusCode" "$_statusCode"
+
+  MAIL_BIN="${MAIL_BIN:-$(_readaccountconf_mutable MAIL_BIN)}"
+  if [ -n "$MAIL_BIN" ] && ! _exists "$MAIL_BIN"; then
+    _err "It seems that the command $MAIL_BIN is not in path."
+    return 1
+  fi
+  _MAIL_CMD=$(_mail_cmnd)
+  if [ -n "$MAIL_BIN" ]; then
+    _saveaccountconf_mutable MAIL_BIN "$MAIL_BIN"
+  else
+    _clearaccountconf "MAIL_BIN"
+  fi
+  _MAIL_BODY=$(_mail_body)
+
+  MAIL_FROM="${MAIL_FROM:-$(_readaccountconf_mutable MAIL_FROM)}"
+  if [ -n "$MAIL_FROM" ]; then
+    if ! _contains "$MAIL_FROM" "@"; then
+      _err "It seems that the MAIL_FROM=$MAIL_FROM is not a valid email address."
+      return 1
+    fi
+
+    _saveaccountconf_mutable MAIL_FROM "$MAIL_FROM"
+  fi
+
+  MAIL_TO="${MAIL_TO:-$(_readaccountconf_mutable MAIL_TO)}"
+  if [ -n "$MAIL_TO" ]; then
+    if ! _contains "$MAIL_TO" "@"; then
+      _err "It seems that the MAIL_TO=$MAIL_TO is not a valid email address."
+      return 1
+    fi
+
+    _saveaccountconf_mutable MAIL_TO "$MAIL_TO"
+  else
+    MAIL_TO="$(_readaccountconf ACCOUNT_EMAIL)"
+    if [ -z "$MAIL_TO" ]; then
+      _err "It seems that account email is empty."
+      return 1
+    fi
+  fi
+
+  contenttype="text/plain; charset=utf-8"
+  subject="=?UTF-8?B?$(echo "$_subject" | _base64)?="
+  result=$({ echo "$_MAIL_BODY" | eval "$_MAIL_CMD"; } 2>&1)
+
+  if [ $? -ne 0 ]; then
+    _debug "mail send error."
+    _err "$result"
+    return 1
+  fi
+
+  _debug "mail send success."
+  return 0
+}
+
+_mail_cmnd() {
+  if [ -n "$MAIL_BIN" ]; then
+    _MAIL_BIN="$MAIL_BIN"
+  elif _exists "sendmail"; then
+    _MAIL_BIN="sendmail"
+  elif _exists "ssmtp"; then
+    _MAIL_BIN="ssmtp"
+  elif _exists "mutt"; then
+    _MAIL_BIN="mutt"
+  elif _exists "mail"; then
+    _MAIL_BIN="mail"
+  else
+    _err "Please install sendmail, ssmtp, mutt or mail first."
+    return 1
+  fi
+
+  case $(basename "$_MAIL_BIN") in
+    sendmail)
+      if [ -n "$MAIL_FROM" ]; then
+        echo "'$_MAIL_BIN' -f '$MAIL_FROM' '$MAIL_TO'"
+      else
+        echo "'$_MAIL_BIN' '$MAIL_TO'"
+      fi
+      ;;
+    ssmtp)
+      echo "'$_MAIL_BIN' '$MAIL_TO'"
+      ;;
+    mutt | mail)
+      echo "'$_MAIL_BIN' -s '$_subject' '$MAIL_TO'"
+      ;;
+    *)
+      _err "Command $MAIL_BIN is not supported, use sendmail, ssmtp, mutt or mail."
+      return 1
+      ;;
+  esac
+}
+
+_mail_body() {
+  if [ "$_MAIL_BIN" = "sendmail" ] || [ "$_MAIL_BIN" = "ssmtp" ]; then
+    if [ -n "$MAIL_FROM" ]; then
+      echo "From: $MAIL_FROM"
+    fi
+
+    echo "To: $MAIL_TO"
+    echo "Subject: $subject"
+    echo "Content-Type: $contenttype"
+    echo
+  fi
+
+  echo "$_content"
+}

+ 131 - 0
notify/mailgun.sh

@@ -0,0 +1,131 @@
+#!/usr/bin/env sh
+
+#Support mailgun.com api
+
+#MAILGUN_API_KEY="xxxx"
+#MAILGUN_TO="yyyy@gmail.com"
+
+#MAILGUN_REGION="us|eu"          #optional, use "us" as default
+#MAILGUN_API_DOMAIN="xxxxxx.com"  #optional, use the default sandbox domain
+#MAILGUN_FROM="xxx@xxxxx.com"    #optional, use the default sendbox account
+
+_MAILGUN_BASE_US="https://api.mailgun.net/v3"
+_MAILGUN_BASE_EU="https://api.eu.mailgun.net/v3"
+
+_MAILGUN_BASE="$_MAILGUN_BASE_US"
+
+# subject  content statusCode
+mailgun_send() {
+  _subject="$1"
+  _content="$2"
+  _statusCode="$3" #0: success, 1: error 2($RENEW_SKIP): skipped
+  _debug "_statusCode" "$_statusCode"
+
+  MAILGUN_API_KEY="${MAILGUN_API_KEY:-$(_readaccountconf_mutable MAILGUN_API_KEY)}"
+  if [ -z "$MAILGUN_API_KEY" ]; then
+    MAILGUN_API_KEY=""
+    _err "You didn't specify a mailgun api key MAILGUN_API_KEY yet ."
+    _err "You can get yours from here https://mailgun.com"
+    return 1
+  fi
+  _saveaccountconf_mutable MAILGUN_API_KEY "$MAILGUN_API_KEY"
+
+  MAILGUN_REGION="${MAILGUN_REGION:-$(_readaccountconf_mutable MAILGUN_REGION)}"
+  if [ -z "$MAILGUN_REGION" ]; then
+    MAILGUN_REGION=""
+    _debug "The MAILGUN_REGION is not set, so use the default us region."
+    _MAILGUN_BASE="$_MAILGUN_BASE_US"
+  else
+    MAILGUN_REGION="$(echo "$MAILGUN_REGION" | _lower_case)"
+    _saveaccountconf_mutable MAILGUN_REGION "$MAILGUN_REGION"
+    if [ "$MAILGUN_REGION" = "us" ]; then
+      _MAILGUN_BASE="$_MAILGUN_BASE_US"
+    else
+      _MAILGUN_BASE="$_MAILGUN_BASE_EU"
+    fi
+  fi
+  _debug _MAILGUN_BASE "$_MAILGUN_BASE"
+  MAILGUN_TO="${MAILGUN_TO:-$(_readaccountconf_mutable MAILGUN_TO)}"
+  if [ -z "$MAILGUN_TO" ]; then
+    MAILGUN_TO=""
+    _err "You didn't specify an email to MAILGUN_TO receive messages."
+    return 1
+  fi
+  _saveaccountconf_mutable MAILGUN_TO "$MAILGUN_TO"
+
+  MAILGUN_API_DOMAIN="${MAILGUN_API_DOMAIN:-$(_readaccountconf_mutable MAILGUN_API_DOMAIN)}"
+  if [ -z "$MAILGUN_API_DOMAIN" ]; then
+    _info "The MAILGUN_API_DOMAIN is not set, try to get the default sending sandbox domain for you."
+    if ! _mailgun_rest GET "/domains"; then
+      _err "Can not get sandbox domain."
+      return 1
+    fi
+    _sendboxDomain="$(echo "$response" | _egrep_o '"name": *"sandbox.*.mailgun.org"' | cut -d : -f 2 | tr -d '" ')"
+    _debug _sendboxDomain "$_sendboxDomain"
+    MAILGUN_API_DOMAIN="$_sendboxDomain"
+    if [ -z "$MAILGUN_API_DOMAIN" ]; then
+      _err "Can not get sandbox domain for MAILGUN_API_DOMAIN"
+      return 1
+    fi
+
+    _info "$(__green "When using sandbox domain, you must verify your email first.")"
+    #todo: add recepient
+  fi
+  if [ -z "$MAILGUN_API_DOMAIN" ]; then
+    _err "Can not get MAILGUN_API_DOMAIN"
+    return 1
+  fi
+  _saveaccountconf_mutable MAILGUN_API_DOMAIN "$MAILGUN_API_DOMAIN"
+
+  MAILGUN_FROM="${MAILGUN_FROM:-$(_readaccountconf_mutable MAILGUN_FROM)}"
+  if [ -z "$MAILGUN_FROM" ]; then
+    MAILGUN_FROM="$PROJECT_NAME@$MAILGUN_API_DOMAIN"
+    _info "The MAILGUN_FROM is not set, so use the default value: $MAILGUN_FROM"
+  else
+    _debug MAILGUN_FROM "$MAILGUN_FROM"
+    _saveaccountconf_mutable MAILGUN_FROM "$MAILGUN_FROM"
+  fi
+
+  #send from url
+  _msg="/$MAILGUN_API_DOMAIN/messages?from=$(printf "%s" "$MAILGUN_FROM" | _url_encode)&to=$(printf "%s" "$MAILGUN_TO" | _url_encode)&subject=$(printf "%s" "$_subject" | _url_encode)&text=$(printf "%s" "$_content" | _url_encode)"
+  _debug "_msg" "$_msg"
+  _mailgun_rest POST "$_msg"
+  if _contains "$response" "Queued. Thank you."; then
+    _debug "mailgun send success."
+    return 0
+  else
+    _err "mailgun send error"
+    _err "$response"
+    return 1
+  fi
+
+}
+
+# method uri  data
+_mailgun_rest() {
+  _method="$1"
+  _mguri="$2"
+  _mgdata="$3"
+  _debug _mguri "$_mguri"
+  _mgurl="$_MAILGUN_BASE$_mguri"
+  _debug _mgurl "$_mgurl"
+
+  _auth="$(printf "%s" "api:$MAILGUN_API_KEY" | _base64)"
+  export _H1="Authorization: Basic $_auth"
+  export _H2="Content-Type: application/json"
+
+  if [ "$_method" = "GET" ]; then
+    response="$(_get "$_mgurl")"
+  else
+    _debug _mgdata "$_mgdata"
+    response="$(_post "$_mgdata" "$_mgurl" "" "$_method")"
+  fi
+  if [ "$?" != "0" ]; then
+    _err "Error: $_mguri"
+    _err "$response"
+    return 1
+  fi
+  _debug2 response "$response"
+  return 0
+
+}

+ 15 - 0
notify/pop.sh

@@ -0,0 +1,15 @@
+#!/usr/bin/env sh
+
+# support pop
+
+pop_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
+}

+ 56 - 0
notify/sendgrid.sh

@@ -0,0 +1,56 @@
+#!/usr/bin/env sh
+
+#Support SENDGRID.com api
+
+#SENDGRID_API_KEY=""
+#SENDGRID_TO="xxxx@xxx.com"
+#SENDGRID_FROM="xxxx@cccc.com"
+
+sendgrid_send() {
+  _subject="$1"
+  _content="$2"
+  _statusCode="$3" #0: success, 1: error 2($RENEW_SKIP): skipped
+  _debug "_statusCode" "$_statusCode"
+
+  SENDGRID_API_KEY="${SENDGRID_API_KEY:-$(_readaccountconf_mutable SENDGRID_API_KEY)}"
+  if [ -z "$SENDGRID_API_KEY" ]; then
+    SENDGRID_API_KEY=""
+    _err "You didn't specify a sendgrid api key SENDGRID_API_KEY yet ."
+    _err "You can get yours from here https://sendgrid.com"
+    return 1
+  fi
+  _saveaccountconf_mutable SENDGRID_API_KEY "$SENDGRID_API_KEY"
+
+  SENDGRID_TO="${SENDGRID_TO:-$(_readaccountconf_mutable SENDGRID_TO)}"
+  if [ -z "$SENDGRID_TO" ]; then
+    SENDGRID_TO=""
+    _err "You didn't specify an email to SENDGRID_TO receive messages."
+    return 1
+  fi
+  _saveaccountconf_mutable SENDGRID_TO "$SENDGRID_TO"
+
+  SENDGRID_FROM="${SENDGRID_FROM:-$(_readaccountconf_mutable SENDGRID_FROM)}"
+  if [ -z "$SENDGRID_FROM" ]; then
+    SENDGRID_FROM=""
+    _err "You didn't specify an email to SENDGRID_FROM receive messages."
+    return 1
+  fi
+  _saveaccountconf_mutable SENDGRID_FROM "$SENDGRID_FROM"
+
+  export _H1="Authorization: Bearer $SENDGRID_API_KEY"
+  export _H2="Content-Type: application/json"
+
+  _content="$(echo "$_content" | _json_encode)"
+  _data="{\"personalizations\": [{\"to\": [{\"email\": \"$SENDGRID_TO\"}]}],\"from\": {\"email\": \"$SENDGRID_FROM\"},\"subject\": \"$_subject\",\"content\": [{\"type\": \"text/plain\", \"value\": \"$_content\"}]}"
+  response="" #just make shellcheck happy
+  if _post "$_data" "https://api.sendgrid.com/v3/mail/send"; then
+    if [ -z "$response" ]; then
+      _info "sendgrid send sccess."
+      return 0
+    fi
+  fi
+  _err "sendgrid send error."
+  _err "$response"
+  return 1
+
+}

+ 55 - 0
notify/slack.sh

@@ -0,0 +1,55 @@
+#!/usr/bin/env sh
+
+#Support Slack webhooks
+
+#SLACK_WEBHOOK_URL=""
+#SLACK_CHANNEL=""
+#SLACK_USERNAME=""
+
+slack_send() {
+  _subject="$1"
+  _content="$2"
+  _statusCode="$3" #0: success, 1: error 2($RENEW_SKIP): skipped
+  _debug "_statusCode" "$_statusCode"
+
+  SLACK_WEBHOOK_URL="${SLACK_WEBHOOK_URL:-$(_readaccountconf_mutable SLACK_WEBHOOK_URL)}"
+  if [ -z "$SLACK_WEBHOOK_URL" ]; then
+    SLACK_WEBHOOK_URL=""
+    _err "You didn't specify a Slack webhook url SLACK_WEBHOOK_URL yet."
+    return 1
+  fi
+  _saveaccountconf_mutable SLACK_WEBHOOK_URL "$SLACK_WEBHOOK_URL"
+
+  SLACK_CHANNEL="${SLACK_CHANNEL:-$(_readaccountconf_mutable SLACK_CHANNEL)}"
+  if [ -n "$SLACK_CHANNEL" ]; then
+    _saveaccountconf_mutable SLACK_CHANNEL "$SLACK_CHANNEL"
+  fi
+
+  SLACK_USERNAME="${SLACK_USERNAME:-$(_readaccountconf_mutable SLACK_USERNAME)}"
+  if [ -n "$SLACK_USERNAME" ]; then
+    _saveaccountconf_mutable SLACK_USERNAME "$SLACK_USERNAME"
+  fi
+
+  export _H1="Content-Type: application/json"
+
+  _content="$(printf "*%s*\n%s" "$_subject" "$_content" | _json_encode)"
+  _data="{\"text\": \"$_content\", "
+  if [ -n "$SLACK_CHANNEL" ]; then
+    _data="$_data\"channel\": \"$SLACK_CHANNEL\", "
+  fi
+  if [ -n "$SLACK_USERNAME" ]; then
+    _data="$_data\"username\": \"$SLACK_USERNAME\", "
+  fi
+  _data="$_data\"mrkdwn\": \"true\"}"
+
+  if _post "$_data" "$SLACK_WEBHOOK_URL"; then
+    # shellcheck disable=SC2154
+    if [ "$response" = "ok" ]; then
+      _info "slack send success."
+      return 0
+    fi
+  fi
+  _err "slack send error."
+  _err "$response"
+  return 1
+}

+ 15 - 0
notify/smtp.sh

@@ -0,0 +1,15 @@
+#!/usr/bin/env sh
+
+# support smtp
+
+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
+}