SendGrid 账号 – 自动化流程 – Bash

前段时间接到来自领导的一个需求,需要对我们在用的 SendGrid 平台实现账号侧(创建 – 配置)自动化流程

经过前期调研已经将官方的API测通

接下来就准备使用 Terraform 完成指定流程

因为 SendGrid 在 Terraform 上并没有发布官方的版本,只能挨个找下最近更新的第三方版本

https://registry.terraform.io/providers/kenzo0107/sendgrid/latest/docs

就在我 tf 代码写好了之后,测试的时候总是返回 api key权限不足,创建不了 subuser, 可是我明明用这个key在postman上是测试成功的,于是只能先怀疑这些 terraform 的供应商(中间换了几个provider都报错)不可用


无奈之下,只能回退到自己编码的局面

#!/usr/bin/env bash
# Author: Weldon
# Date: 2024/08/29

SENDGRID_API_KEY="you_api_key"
base_url="https://api.sendgrid.com"
subuser_name="your_name"
subuser_email="your_email_adress"
subuser_password="your_password"

whitelabel_domain="noreply.yourdomain.com"

branded_link_domain="yourdomain.com"
branded_link_subdomain="mail"

cloudflare_base_url="https://api.cloudflare.com"
cloudflare_access_token="your_cf_token"
cloudflare_zone_id="your_zone_id"

yellow() {
  echo -e "\e[33mINFO: $@\e[0m"
}

green() {
  echo -e "\e[32m$@\e[0m\n"
}

red() {
  echo -e "\e[31mBLOCK: $@\e[0m\n"
}

function user_confirm_before_continue() {
  while true; do
    read -p "Do you want to continue? (Y/N): " user_input

    case $user_input in
    [yY] | [yY][eE][sS])
      green "Let's move on, continuing..."
      return 0
      ;;
    *)
      red "Goodbye, exiting..."
      exit 1
      ;;
    esac
  done
}

function user_confirm_before_delete() {
  can_be_delete=0
  while true; do
    read -p "Do you want to delete the previous one? (Y/N): " user_input

    case $user_input in
    [yY] | [yY][eE][sS])
      green "Start deleting..."
      can_be_delete=1
      return 0
      ;;
    *)
      green "Keep intact, Goodbye..."
      exit 1
      ;;
    esac
  done
}

function check_if_request_successful() {
  if [[ $response_code -eq 200 || $response_code -eq 204 ]]; then
    green "---- Congratulations: current http request successful! 👏👏👏"
  else
    red "---- Unfortunate: current http request failed! ❌❌❌"
    exit 1
  fi
}

# Cloudflare API
##

function delete_a_cloudflare_dns_record() {
  cloudflare_dns_record_id=$1

  if [[ -n "${cloudflare_dns_record_id}" ]]; then
    yellow "---- Going to delete cloudflare dns record: ${cloudflare_dns_record_id}"
    response_code=$(curl -s -L -m 10 -o /dev/null -w "%{http_code}" \
      --request DELETE \
      --url "${cloudflare_base_url}/client/v4/zones/${cloudflare_zone_id}/dns_records/${cloudflare_dns_record_id}" \
      --header 'Content-Type: application/json' \
      --header 'X-Auth-Email: ' \
      --header "Authorization: Bearer ${cloudflare_access_token}")
    check_if_request_successful
  fi
}

## List DNS Records
function list_cloudflare_dns_records() {
  page=1
  while true; do
    # Get dns records for the current page
    response=$(curl -s -L --request GET \
      --url "${cloudflare_base_url}/client/v4/zones/${cloudflare_zone_id}/dns_records?type=CNAME&per_page=5&page=${page}" \
      --header 'Content-Type: application/json' \
      --header 'X-Auth-Email: ' \
      --header "Authorization: Bearer ${cloudflare_access_token}")

    # Check if there are no dns records or we've reached the last page
    if [[ $(jq '.result | length' <<<"$response") -eq 0 ]]; then
      break
    fi

    # Extract dns records host/data and output
    jq -r '.result[] | select(.comment == "Domain verification record")| .id + " " + .name + " " + .content' <<<"$response"

    ((page++))
  done
}

function check_cloudflare_dns_records_existed() {
  current_cloudflare_dns_recoed_file=./cloudflare_dns_records_$(date '+%Y-%m-%d-%H:%M:%S')
  yellow "---- All of Cloudflare dns records:"
  list_cloudflare_dns_records | tee $current_cloudflare_dns_recoed_file

  expect_content=$1
  good_to_go=0

  # yellow "---- All of Cloudflare dns records:\n $(cat $current_cloudflare_dns_recoed_file)"

  dns_record_host=$(echo "$expect_content" | awk '{print $1}')
  # if [[ -z $(echo $all_dns_content | jq -r '.result[].name + " " + .result[].content' | grep -w "$dns_record_host") ]]; then
  # if [[ -z $(echo $all_dns_content | jq -r '.result[].name + " " + .result[].content' | grep -w "$expect_content") ]]; then
  if [[ -z $(cat "$current_cloudflare_dns_recoed_file" | grep -w "$dns_record_host") ]]; then
    green "---- Record has not been created before with host: $dns_record_host"
    good_to_go=1
  else
    red "---- Previous record already existed with host : $dns_record_host"
    user_confirm_before_delete
    if [[ $can_be_delete -eq 1 ]]; then
      yellow "Let's delete it."
      # dns_record_id=$(echo $all_dns_content | jq -r ".result[] | select(.name==\"$dns_record_host\") | .id")

      dns_record_id=$(cat "$current_cloudflare_dns_recoed_file" | grep -w "$dns_record_host" | awk '{print $1}')
      delete_a_cloudflare_dns_record "$dns_record_id"

      if [ $? -eq 0 ]; then
        yellow "Let's move on."
        good_to_go=1
      fi

    fi
  fi

}

## Create DNS Record
function create_cloudflare_dns_record() {
  [[ -z "$cloudflare_zone_id" ]] && echo "cloudflare_zone_id is empty, exiting..." && exit 1

  # green "---- inner cloudflare func - current sendgrid dns record count: ${#dns_records[@]}"
  if [[ ${#dns_records[@]} -gt 0 ]]; then
    yellow "------------------------------------Start to create clareflare dns record------------------------------------"

    # "${!dns_records[@]}" 和 "${dns_records[@]}" 的区别,它们分别用于获取关联数组的键和值:
    # "${!dns_records[@]}":这将获取关联数组 dns_records 的所有键。在我们的例子中,这些键是 host。
    # "${dns_records[@]}":这将获取关联数组 dns_records 的所有值。在我们的例子中,这些值是 data。
    for host in "${!dns_records[@]}"; do
      yellow "---- Current host is: $host"
      yellow "---- Current data is: ${dns_records[$host]}"

      go_to_check="${host} ${dns_records[$host]}"
      check_cloudflare_dns_records_existed "$go_to_check"
      if [[ $good_to_go -eq 1 ]]; then
        response_by_create_cloudflare_dns_record=$(curl -s -L -m 10 \
          -X POST \
          "${cloudflare_base_url}/client/v4/zones/${cloudflare_zone_id}/dns_records" \
          --header 'Content-Type: application/json' \
          --header 'X-Auth-Email: ' \
          --header "Authorization: Bearer ${cloudflare_access_token}" \
          --data '{
            "content": '\"${dns_records[$host]}\"',
            "name": '\"${host}\"',
            "proxied": false,
            "type": "CNAME",
            "comment": "Domain verification record",
            "id": "023e105f4ecef8ad9ca31a8372d0c355",
            "tags": [],
            "ttl": 3600
          }')
        yellow "---- Result of create cloudflare dns record: \n $response_by_create_cloudflare_dns_record"
      fi
    done
  else
    red "---- Not found Cloudflare dns datas given by SendGrid, exiting..."
    exit 1
  fi
}

# Settings - Subusers
## List all Subusers
function check_subuser_existed() {
  expected_subuser=$1
  yellow "---- Going to get all subusers"
  all_users=$(curl -s "${base_url}/v3/subusers" --header "Authorization: Bearer ${SENDGRID_API_KEY}" | jq -r .[].username)
  yellow "SendGrid all subusers:\n $all_users"
  if [[ -z $(echo $all_users | grep -w $expected_subuser) ]]; then
    green "---- Subuser has not been created before: $expected_subuser"
  else
    red "---- Previous subuser already existed: $expected_subuser"
    user_confirm_before_delete
    if [[ $can_be_delete -eq 1 ]]; then
      yellow "Let's delete it."
      delete_a_subuser
      if [ $? -eq 0 ]; then
        yellow "Let's move on."
        good_to_go=1
      fi
    fi
  fi
}

## Retrieve all assigned IPs
function retrieve_all_assigned_ips() {
  yellow "---- Going to get all assigned ips"

  all_assigned_ips=$(curl -s -X GET "${base_url}/v3/ips/assigned" --header "Authorization: Bearer ${SENDGRID_API_KEY}" | jq -r .[].ip)
  yellow "---- Current all assigned ips: \n$all_assigned_ips"
  dedicated_ip=$(echo $all_assigned_ips | awk '{print $1}')
  if [[ -n "${dedicated_ip}" ]]; then
    yellow "---- Going to use the first assigned ip: $dedicated_ip"
  else
    red "---- Not found assigned ip at main account side, exiting..."
    exit 1
  fi

}

## Create Subuser
function create_a_subuser() {
  [[ -z "$subuser_name" ]] && echo "subuser_name is empty, exiting..." && exit 1 || [[ -z "$subuser_email" ]] && echo "subuser_email is empty, exiting..." && exit 1 || [[ -z "$subuser_password" ]] && echo "subuser_password is empty, exiting..." && exit 1

  check_subuser_existed "${subuser_name}"
  retrieve_all_assigned_ips

  yellow "---- Going to create subuser: ${subuser_name}"
  subuser_info=$(curl -s -X POST \
    "${base_url}/v3/subusers" \
    --header "Content-Type: application/json" \
    --header "Authorization: Bearer ${SENDGRID_API_KEY}" \
    --data-raw '{
    "username": '\"${subuser_name}\"',
    "email": '\"${subuser_email}\"',
    "password": '\"${subuser_password}\"',
    "ips": [
      '\"${dedicated_ip}\"'
    ],
    "region": "global",
    "include_region": true
  }')
  yellow "---- Subuser(${subuser_name}) information:\n $subuser_info"
  if [[ -n $(echo $subuser_info | grep -i error) ]]; then
    red "Create useruser failed, please check it manually..."
    exit 1
  else
    if [[ -z "echo ${subuser_info}" ]]; then
      echo "string is not empty"
    fi

    subuser_id=$(echo $subuser_info | jq -r .user_id)
    green "${subuser_name}'s user_id is: $subuser_id"
  fi

}

## Delete a subuser
function delete_a_subuser() {
  yellow "---- Going to delete subuser: ${subuser_name}"

  response_code=$(curl -s -L -m 10 -o /dev/null -w "%{http_code}" \
    --request DELETE "${base_url}/v3/subusers/${subuser_name}" \
    --header "Authorization: Bearer ${SENDGRID_API_KEY}")

  check_if_request_successful
}

## Retrieve Subuser Reputations
function retrieve_subuser_reputations() {
  yellow "---- Going to get reputations for: ${subuser_name}"

  curl -s "${base_url}/v3/subusers/reputations?usernames=${subuser_name}" \
    --header "Authorization: Bearer ${SENDGRID_API_KEY}"
}

## Enable/disable a subuser
function enable_disable_a_subuser() {
  action=$1
  yellow "---- Going to $action ${subuser_name}"
  user_confirm_before_continue

  is_disabled=false
  if [[ $action == 'enable' ]]; then
    is_disabled=false
  elif [[ $action == 'disable' ]]; then
    is_disabled=true
  fi

  curl -s --request PATCH "${base_url}/v3/subusers/${subuser_name}" \
    --header "Content-Type: application/json" \
    --header "Authorization: Bearer ${SENDGRID_API_KEY}" \
    --data "{
    \"disabled\": $is_disabled
  }"
}

# Security - Api key
## Create API keys
function create_sendgrid_api_keys() {
  yellow "---- Going to create sendgrid api key for: ${subuser_name}"
  SUBUSER_API_KEY_NAME="Notification Key"
  retrieve_all_api_keys_belonging_to_a_authenticated_user "$SUBUSER_API_KEY_NAME"

  response_by_create_sendgrid_api_keys=$(curl -s -X POST \
    "${base_url}/v3/api_keys" \
    --header "on-behalf-of: ${subuser_name}" \
    --header "Content-Type: application/json" \
    --header "Authorization: Bearer ${SENDGRID_API_KEY}" \
    --data "{
    \"name\": \"$SUBUSER_API_KEY_NAME\",
    \"scopes\": [
      \"mail.send\",
      \"alerts.create\",
      \"alerts.read\"
    ]
  }")

  echo $response_by_create_sendgrid_api_keys >>./${subuser_name}_sendgrid_api_keys.secret
  echo -e "---- Create at $(date '+%Y-%m-%d %H:%M:%S') \n" >>./${subuser_name}_sendgrid_api_keys.secret
}

## Retrieve all API Keys belonging to the authenticated user
function retrieve_all_api_keys_belonging_to_a_authenticated_user() {
  api_key_name=$1
  yellow "---- Going to get all sendgrid api keys belonging to: ${subuser_name}"

  all_api_keys_with_a_subuser=$(curl -s "${base_url}/v3/api_keys" \
    --header "on-behalf-of: ${subuser_name}" \
    --header "Authorization: Bearer ${SENDGRID_API_KEY}")

  if [[ -n $(echo $all_api_keys_with_a_subuser | grep -i error) ]]; then
    red "Get all sendgrid api keys failed, please check it manually..."
    exit 1
  else
    yellow "---- All Api keys for ${subuser_name}: $all_api_keys_with_a_subuser"
    if [[ -z $(echo $all_api_keys_with_a_subuser | grep -w "$api_key_name") ]]; then
      green "---- Api key has not been created before: $api_key_name"
    else
      red "---- Previous Api key already existed: $api_key_name"
      user_confirm_before_delete
      if [[ $can_be_delete -eq 1 ]]; then
        yellow "Let's delete it."
        api_key_id=$(echo $all_api_keys_with_a_subuser | jq -r ".result[] | select(.name==\"$api_key_name\") | .api_key_id")
        delete_a_sendgrid_api_keys "$api_key_id"
        if [ $? -eq 0 ]; then
          yellow "Let's move on."
          good_to_go=1
        fi
      fi
    fi
  fi
}

## Delete API keys
function delete_a_sendgrid_api_keys() {
  expected_api_key_id=$1
  yellow "---- Going to delete a sendgrid api key belonging to: ${subuser_name}"

  response_code=$(curl -s -L -m 10 -o /dev/null -w "%{http_code}" \
    --globoff --request DELETE "${base_url}/v3/api_keys/${expected_api_key_id}" \
    --header "on-behalf-of: ${subuser_name}" \
    --header "Authorization: Bearer ${SENDGRID_API_KEY}")

  check_if_request_successful
}

# Deliverability - Domain Authentication
## List all authenticated domains
function list_authenticated_domains() {
  yellow "---- Going to get all authenticated domains for: ${subuser_name}"
  expected_authenticated_domain=$1

  all_authenticated_domains=$(curl -s "${base_url}/v3/whitelabel/domains" \
    --header "on-behalf-of: ${subuser_name}" \
    --header "Authorization: Bearer ${SENDGRID_API_KEY}")

  if [[ -n $(echo $all_authenticated_domains | grep -i error) ]]; then
    red "Get all authenticated domains failed, please check it manually..."
    exit 1
  else
    yellow "---- all authenticated domain for ${subuser_name}:\n $all_authenticated_domains"
    if [[ -z $(echo $all_authenticated_domains | grep -w $expected_authenticated_domain) ]]; then
      green "---- Authenticated domain has not been created before: $expected_authenticated_domain"
    else
      red "---- Previous authenticated domain already existed: $expected_authenticated_domain"
      user_confirm_before_delete
      if [[ $can_be_delete -eq 1 ]]; then
        yellow "Let's delete it."
        authenticated_domain_id=$(echo $all_authenticated_domains | jq -r ".[] | select(.domain==\"$expected_authenticated_domain\") | .id")
        delete_an_authenticated_domain $authenticated_domain_id
        if [ $? -eq 0 ]; then
          yellow "Let's move on."
          good_to_go=1
        fi
      fi
    fi
  fi
}

## Authenticate a domain
function authenticate_a_domain() {
  yellow "---- Going to add an authenticated domain \e[0;35m${whitelabel_domain}\e[0m for: ${subuser_name}"
  list_authenticated_domains "${whitelabel_domain}"

  response=$(curl -s -X POST \
    "${base_url}/v3/whitelabel/domains" \
    --header "on-behalf-of: ${subuser_name}" \
    --header "Content-Type: application/json" \
    --header "Authorization: Bearer ${SENDGRID_API_KEY}" \
    --data "{
    \"domain\": \"${whitelabel_domain}\",
    \"default\": true
  }")

  if [[ -n $(echo $response | grep -i error) ]]; then
    red "Add an authenticated domain failed, please check it manually..."
    exit 1
  else
    yellow "---- Response by authenticate a domain: $response"

    authenticated_domain_id=$(echo "$response" | jq -r '.id')
    yellow "---- Authenticated domain ID: $authenticated_domain_id"

    for entry in mail_cname dkim1 dkim2; do
      sendgrid_dns_host=$(echo "$response" | jq -r ".dns.${entry}.host")
      sendgrid_dns_data=$(echo "$response" | jq -r ".dns.${entry}.data")
      dns_records["$sendgrid_dns_host"]="$sendgrid_dns_data"
    done
    # green "---- inner sendgrid func - current sendgrid dns record count: ${#dns_records[@]}"
  fi
}

## Validate a domain authentication.
function validate_a_domain_authentication() {
  yellow "---- Going to validate a domain authentication \e[0;35m${whitelabel_domain}\e[0m for: ${subuser_name}"

  if [[ -n "${authenticated_domain_id}" ]]; then
    result_of_validate_domain_authentication=$(curl -s -X POST \
      "${base_url}/v3/whitelabel/domains/${authenticated_domain_id}/validate" \
      --header "on-behalf-of: ${subuser_name}" \
      --header "Authorization: Bearer ${SENDGRID_API_KEY}")

    yellow "---- Result of validate domain authentication: $result_of_validate_domain_authentication"
    if $(echo "$result_of_validate_domain_authentication" | jq -r .valid); then
      green "---- Great, domain authentication have been set up successfully."
    else
      red "---- Sadly, domain authentication set up failed."
    fi
  else
    red "Not found authenticated_domain_id, exiting..."
    exit 1
  fi
}

## Delete an authenticated domain
function delete_an_authenticated_domain() {
  expected_authenticated_domain_id=$1
  yellow "---- Going to delete an authenticated domain for: ${subuser_name}"

  if [[ -n "${expected_authenticated_domain_id}" ]]; then
    response_code=$(curl -s -L -m 10 -o /dev/null -w "%{http_code}" \
      --request DELETE "${base_url}/v3/whitelabel/domains/${expected_authenticated_domain_id}" \
      --header "on-behalf-of: ${subuser_name}" \
      --header "Authorization: Bearer ${SENDGRID_API_KEY}")

    check_if_request_successful
  else
    red "Not found authenticated_domain_id, exiting..."
    exit 1
  fi

}

# Deliverability - Link Branding
## Retrieve all branded links
function retrieve_all_branded_links() {
  expected_branded_link_subdomain=$1
  expected_branded_link_domain=$2
  yellow "---- Going to get all branded links for: ${subuser_name}"
  all_branded_links=$(curl -s "${base_url}/v3/whitelabel/links" \
    --header "on-behalf-of: ${subuser_name}" \
    --header "Authorization: Bearer ${SENDGRID_API_KEY}")

  yellow "---- all branded links for ${subuser_name}:\n $all_branded_links"
  if [[ -z $(echo $all_branded_links | grep -w $expected_branded_link_subdomain | grep -w $expected_branded_link_domain) ]]; then
    green "---- Branded link has not been created before: ${expected_branded_link_subdomain}.${expected_branded_link_domain}"
  else
    red "---- Previous branded link already existed: ${expected_branded_link_subdomain}.${expected_branded_link_domain}"
    user_confirm_before_delete
    if [[ $can_be_delete -eq 1 ]]; then
      yellow "Let's delete it."
      previous_branded_link_id=$(echo $all_branded_links | jq -r ".[] | select(.subdomain==\"$expected_branded_link_subdomain\" and .domain==\"$expected_branded_link_domain\") | .id")
      delete_a_branded_link "$previous_branded_link_id"
      if [ $? -eq 0 ]; then
        yellow "Let's move on."
        good_to_go=1
      fi
    fi
  fi
}

## Create a branded link
function create_a_branded_link() {

  if [[ -n "${branded_link_domain}" && -n "${branded_link_subdomain}" ]]; then
    retrieve_all_branded_links "${branded_link_subdomain}" "${branded_link_domain}"
    yellow "---- Going to create a branded link \e[0;35m${branded_link_subdomain}\e[0m for: ${subuser_name}"
    reponse_by_create_a_branded_link=$(curl -s -X POST \
      "${base_url}/v3/whitelabel/links" \
      --header "on-behalf-of: ${subuser_name}" \
      --header "Content-Type: application/json" \
      --header "Authorization: Bearer ${SENDGRID_API_KEY}" \
      --data '{
      "domain": '\"${branded_link_domain}\"',
      "subdomain": '\"${branded_link_subdomain}\"',
      "default": true
    }')
  else
    red "Not found branded_link_domain or branded_link_subdomain, exiting..."
    exit 1
  fi

  if [[ -n $(echo $reponse_by_create_a_branded_link | grep -i error) ]]; then
    red "Add an authenticated domain failed, please check it manually..."
    exit 1
  else
    yellow "---- Response by create a branded link: $reponse_by_create_a_branded_link"

    branded_link_id=$(echo "$reponse_by_create_a_branded_link" | jq -r '.id')
    yellow "---- Authenticated domain ID: $branded_link_id"

    for entry in domain_cname owner_cname; do
      sendgrid_dns_host=$(echo "$reponse_by_create_a_branded_link" | jq -r ".dns.${entry}.host")
      sendgrid_dns_data=$(echo "$reponse_by_create_a_branded_link" | jq -r ".dns.${entry}.data")
      dns_records["$sendgrid_dns_host"]="$sendgrid_dns_data"
    done
    # green "---- inner sendgrid func - current sendgrid dns record count: ${#dns_records[@]}"
  fi
}

## Validate a branded link
function validate_a_branded_link() {
  yellow "---- Going to validate a branded link for: ${subuser_name}"

  if [[ -n "${branded_link_id}" ]]; then
    result_of_validate_branded_link=$(curl -s -X POST \
      "${base_url}/v3/whitelabel/links/${branded_link_id}/validate" \
      --header "on-behalf-of: ${subuser_name}" \
      --header "Authorization: Bearer ${SENDGRID_API_KEY}")

    yellow "---- Result of validate branded link: $result_of_validate_branded_link"
    if $(echo "$result_of_validate_branded_link" | jq -r .valid); then
      green "---- Great, branded link have been set up successfully."
    else
      red "---- Sadly, branded link set up failed."
    fi
  else
    red "Not found branded_link_id, exiting..."
    exit 1
  fi
}

## Delete a branded link
function delete_a_branded_link() {
  expected_branded_link_id=$1
  yellow "---- Going to delete a branded link for: ${subuser_name}"

  if [[ -n "${expected_branded_link_id}" ]]; then
    response_code=$(curl -s -L -m 10 -o /dev/null -w "%{http_code}" \
      --globoff --request DELETE "${base_url}/v3/whitelabel/links/${expected_branded_link_id}" \
      --header "on-behalf-of: ${subuser_name}" \
      --header "Authorization: Bearer ${SENDGRID_API_KEY}")
    check_if_request_successful
  else
    red "Not found branded_link_id, exiting..."
    exit 1
  fi

}

# main process to create a subuser
function main() {
  declare -A dns_records=()

  create_a_subuser
  create_sendgrid_api_keys

  authenticate_a_domain
  # Here, requires going to cloudflare to set up DNS and waiting for a while
  create_cloudflare_dns_record
  sleep 60
  validate_a_domain_authentication

  unset dns_records
  declare -A dns_records=()
  create_a_branded_link
  # Here, requires going to cloudflare to set up DNS and waiting for a while
  create_cloudflare_dns_record
  sleep 60
  validate_a_branded_link

  retrieve_subuser_reputations

  # enable_disable_a_subuser disable
  # enable_disable_a_subuser enable
}

main

局部截图

本脚本涉及逻辑诸多

  1. 创建 SendGrid subuser
    • 检查是否已存在
    • 用户确认删除
  2. 创建 SendGrid API key
    • 检查是否已存在
    • 用户确认删除
  3. 创建 SendGrid 验证域名
    • 检查是否已存在
    • 用户确认删除
  4. 创建 Cloudflare dns record
    • 检查是否已存在
    • 用户确认删除
  5. 验证 SendGrid 验证域名
  6. 创建 SendGrid 品牌链接
    • 检查是否已存在
    • 用户确认删除
  7. 创建 Cloudflare dns record
    • 检查是否已存在
    • 用户确认删除
  8. 验证 SendGrid 品牌链接
  9. 获取 SendGrid subuser 声誉度

客官慢走,有空常来啊😄
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇