You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

388 lines
12 KiB

  1. #!/usr/bin/env sh
  2. # support smtp
  3. # Please report bugs to https://github.com/acmesh-official/acme.sh/issues/3358
  4. # This implementation uses either curl or Python (3 or 2.7).
  5. # (See also the "mail" notify hook, which supports other ways to send mail.)
  6. # SMTP_FROM="from@example.com" # required
  7. # SMTP_TO="to@example.com" # required
  8. # SMTP_HOST="smtp.example.com" # required
  9. # SMTP_PORT="25" # defaults to 25, 465 or 587 depending on SMTP_SECURE
  10. # SMTP_SECURE="none" # one of "none", "ssl" (implicit TLS, TLS Wrapper), "tls" (explicit TLS, STARTTLS)
  11. # SMTP_USERNAME="" # set if SMTP server requires login
  12. # SMTP_PASSWORD="" # set if SMTP server requires login
  13. # SMTP_TIMEOUT="30" # seconds for SMTP operations to timeout
  14. # SMTP_BIN="/path/to/curl_or_python" # default finds first of curl, python3, or python on PATH
  15. SMTP_SECURE_DEFAULT="none"
  16. SMTP_TIMEOUT_DEFAULT="30"
  17. # subject content statuscode
  18. smtp_send() {
  19. SMTP_SUBJECT="$1"
  20. SMTP_CONTENT="$2"
  21. # UNUSED: _statusCode="$3" # 0: success, 1: error 2($RENEW_SKIP): skipped
  22. # Load and validate config:
  23. SMTP_BIN="$(_readaccountconf_mutable_default SMTP_BIN)"
  24. if [ -n "$SMTP_BIN" ] && ! _exists "$SMTP_BIN"; then
  25. _err "SMTP_BIN '$SMTP_BIN' does not exist."
  26. return 1
  27. fi
  28. if [ -z "$SMTP_BIN" ]; then
  29. # Look for a command that can communicate with an SMTP server.
  30. # (Please don't add sendmail, ssmtp, mutt, mail, or msmtp here.
  31. # Those are already handled by the "mail" notify hook.)
  32. for cmd in curl python3 python2.7 python pypy3 pypy; do
  33. if _exists "$cmd"; then
  34. SMTP_BIN="$cmd"
  35. break
  36. fi
  37. done
  38. if [ -z "$SMTP_BIN" ]; then
  39. _err "The smtp notify-hook requires curl or Python, but can't find any."
  40. _err 'If you have one of them, define SMTP_BIN="/path/to/curl_or_python".'
  41. _err 'Otherwise, see if you can use the "mail" notify-hook instead.'
  42. return 1
  43. fi
  44. fi
  45. _debug SMTP_BIN "$SMTP_BIN"
  46. _saveaccountconf_mutable_default SMTP_BIN "$SMTP_BIN"
  47. SMTP_FROM="$(_readaccountconf_mutable_default SMTP_FROM)"
  48. if [ -z "$SMTP_FROM" ]; then
  49. _err "You must define SMTP_FROM as the sender email address."
  50. return 1
  51. fi
  52. _debug SMTP_FROM "$SMTP_FROM"
  53. _saveaccountconf_mutable_default SMTP_FROM "$SMTP_FROM"
  54. SMTP_TO="$(_readaccountconf_mutable_default SMTP_TO)"
  55. if [ -z "$SMTP_TO" ]; then
  56. _err "You must define SMTP_TO as the recipient email address."
  57. return 1
  58. fi
  59. _debug SMTP_TO "$SMTP_TO"
  60. _saveaccountconf_mutable_default SMTP_TO "$SMTP_TO"
  61. SMTP_HOST="$(_readaccountconf_mutable_default SMTP_HOST)"
  62. if [ -z "$SMTP_HOST" ]; then
  63. _err "You must define SMTP_HOST as the SMTP server hostname."
  64. return 1
  65. fi
  66. _debug SMTP_HOST "$SMTP_HOST"
  67. _saveaccountconf_mutable_default SMTP_HOST "$SMTP_HOST"
  68. SMTP_SECURE="$(_readaccountconf_mutable_default SMTP_SECURE "$SMTP_SECURE_DEFAULT")"
  69. case "$SMTP_SECURE" in
  70. "none") smtp_port_default="25" ;;
  71. "ssl") smtp_port_default="465" ;;
  72. "tls") smtp_port_default="587" ;;
  73. *)
  74. _err "Invalid SMTP_SECURE='$SMTP_SECURE'. It must be 'ssl', 'tls' or 'none'."
  75. return 1
  76. ;;
  77. esac
  78. _debug SMTP_SECURE "$SMTP_SECURE"
  79. _saveaccountconf_mutable_default SMTP_SECURE "$SMTP_SECURE" "$SMTP_SECURE_DEFAULT"
  80. SMTP_PORT="$(_readaccountconf_mutable_default SMTP_PORT "$smtp_port_default")"
  81. case "$SMTP_PORT" in
  82. *[!0-9]*)
  83. _err "Invalid SMTP_PORT='$SMTP_PORT'. It must be a port number."
  84. return 1
  85. ;;
  86. esac
  87. _debug SMTP_PORT "$SMTP_PORT"
  88. _saveaccountconf_mutable_default SMTP_PORT "$SMTP_PORT" "$smtp_port_default"
  89. SMTP_USERNAME="$(_readaccountconf_mutable_default SMTP_USERNAME)"
  90. _debug SMTP_USERNAME "$SMTP_USERNAME"
  91. _saveaccountconf_mutable_default SMTP_USERNAME "$SMTP_USERNAME"
  92. SMTP_PASSWORD="$(_readaccountconf_mutable_default SMTP_PASSWORD)"
  93. _secure_debug SMTP_PASSWORD "$SMTP_PASSWORD"
  94. _saveaccountconf_mutable_default SMTP_PASSWORD "$SMTP_PASSWORD"
  95. SMTP_TIMEOUT="$(_readaccountconf_mutable_default SMTP_TIMEOUT "$SMTP_TIMEOUT_DEFAULT")"
  96. _debug SMTP_TIMEOUT "$SMTP_TIMEOUT"
  97. _saveaccountconf_mutable_default SMTP_TIMEOUT "$SMTP_TIMEOUT" "$SMTP_TIMEOUT_DEFAULT"
  98. SMTP_X_MAILER="${PROJECT_NAME} ${VER} --notify-hook smtp"
  99. # Run with --debug 2 (or above) to echo the transcript of the SMTP session.
  100. # Careful: this may include SMTP_PASSWORD in plaintext!
  101. if [ "${DEBUG:-$DEBUG_LEVEL_NONE}" -ge "$DEBUG_LEVEL_2" ]; then
  102. SMTP_SHOW_TRANSCRIPT="True"
  103. else
  104. SMTP_SHOW_TRANSCRIPT=""
  105. fi
  106. _debug SMTP_SUBJECT "$SMTP_SUBJECT"
  107. _debug SMTP_CONTENT "$SMTP_CONTENT"
  108. # Send the message:
  109. case "$(basename "$SMTP_BIN")" in
  110. curl) _smtp_send=_smtp_send_curl ;;
  111. py*) _smtp_send=_smtp_send_python ;;
  112. *)
  113. _err "Can't figure out how to invoke '$SMTP_BIN'."
  114. _err "Check your SMTP_BIN setting."
  115. return 1
  116. ;;
  117. esac
  118. if ! smtp_output="$($_smtp_send)"; then
  119. _err "Error sending message with $SMTP_BIN."
  120. if [ -n "$smtp_output" ]; then
  121. _err "$smtp_output"
  122. fi
  123. return 1
  124. fi
  125. return 0
  126. }
  127. ##
  128. ## curl smtp sending
  129. ##
  130. # Send the message via curl using SMTP_* variables
  131. _smtp_send_curl() {
  132. # curl passes --mail-from and --mail-rcpt directly to the SMTP protocol without
  133. # additional parsing, and SMTP requires addr-spec only (no display names).
  134. # In the future, maybe try to parse the addr-spec out for curl args (non-trivial).
  135. if _email_has_display_name "$SMTP_FROM"; then
  136. _err "curl smtp only allows a simple email address in SMTP_FROM."
  137. _err "Change your SMTP_FROM='$SMTP_FROM' to remove the display name."
  138. return 1
  139. fi
  140. if _email_has_display_name "$SMTP_TO"; then
  141. _err "curl smtp only allows simple email addresses in SMTP_TO."
  142. _err "Change your SMTP_TO='$SMTP_TO' to remove the display name(s)."
  143. return 1
  144. fi
  145. # Build curl args in $@
  146. case "$SMTP_SECURE" in
  147. none)
  148. set -- --url "smtp://${SMTP_HOST}:${SMTP_PORT}"
  149. ;;
  150. ssl)
  151. set -- --url "smtps://${SMTP_HOST}:${SMTP_PORT}"
  152. ;;
  153. tls)
  154. set -- --url "smtp://${SMTP_HOST}:${SMTP_PORT}" --ssl-reqd
  155. ;;
  156. *)
  157. # This will only occur if someone adds a new SMTP_SECURE option above
  158. # without updating this code for it.
  159. _err "Unhandled SMTP_SECURE='$SMTP_SECURE' in _smtp_send_curl"
  160. _err "Please re-run with --debug and report a bug."
  161. return 1
  162. ;;
  163. esac
  164. set -- "$@" \
  165. --upload-file - \
  166. --mail-from "$SMTP_FROM" \
  167. --max-time "$SMTP_TIMEOUT"
  168. # Burst comma-separated $SMTP_TO into individual --mail-rcpt args.
  169. _to="${SMTP_TO},"
  170. while [ -n "$_to" ]; do
  171. _rcpt="${_to%%,*}"
  172. _to="${_to#*,}"
  173. set -- "$@" --mail-rcpt "$_rcpt"
  174. done
  175. _smtp_login="${SMTP_USERNAME}:${SMTP_PASSWORD}"
  176. if [ "$_smtp_login" != ":" ]; then
  177. set -- "$@" --user "$_smtp_login"
  178. fi
  179. if [ "$SMTP_SHOW_TRANSCRIPT" = "True" ]; then
  180. set -- "$@" --verbose
  181. else
  182. set -- "$@" --silent --show-error
  183. fi
  184. raw_message="$(_smtp_raw_message)"
  185. _debug2 "curl command:" "$SMTP_BIN" "$*"
  186. _debug2 "raw_message:\n$raw_message"
  187. echo "$raw_message" | "$SMTP_BIN" "$@"
  188. }
  189. # Output an RFC-822 / RFC-5322 email message using SMTP_* variables
  190. _smtp_raw_message() {
  191. echo "From: $SMTP_FROM"
  192. echo "To: $SMTP_TO"
  193. echo "Subject: $(_mime_encoded_word "$SMTP_SUBJECT")"
  194. echo "Date: $(_rfc2822_date)"
  195. echo "Content-Type: text/plain; charset=utf-8"
  196. echo "X-Mailer: $SMTP_X_MAILER"
  197. echo
  198. echo "$SMTP_CONTENT"
  199. }
  200. # Convert text to RFC-2047 MIME "encoded word" format if it contains non-ASCII chars
  201. # text
  202. _mime_encoded_word() {
  203. _text="$1"
  204. # (regex character ranges like [a-z] can be locale-dependent; enumerate ASCII chars to avoid that)
  205. _ascii='] $`"'"[!#%&'()*+,./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ~^_abcdefghijklmnopqrstuvwxyz{|}~-"
  206. if expr "$_text" : "^.*[^$_ascii]" >/dev/null; then
  207. # At least one non-ASCII char; convert entire thing to encoded word
  208. printf "%s" "=?UTF-8?B?$(printf "%s" "$_text" | _base64)?="
  209. else
  210. # Just printable ASCII, no conversion needed
  211. printf "%s" "$_text"
  212. fi
  213. }
  214. # Output current date in RFC-2822 Section 3.3 format as required in email headers
  215. # (e.g., "Mon, 15 Feb 2021 14:22:01 -0800")
  216. _rfc2822_date() {
  217. # Notes:
  218. # - this is deliberately not UTC, because it "SHOULD express local time" per spec
  219. # - the spec requires weekday and month in the C locale (English), not localized
  220. # - this date format specifier has been tested on Linux, Mac, Solaris and FreeBSD
  221. _old_lc_time="$LC_TIME"
  222. LC_TIME=C
  223. date +'%a, %-d %b %Y %H:%M:%S %z'
  224. LC_TIME="$_old_lc_time"
  225. }
  226. # Simple check for display name in an email address (< > or ")
  227. # email
  228. _email_has_display_name() {
  229. _email="$1"
  230. expr "$_email" : '^.*[<>"]' >/dev/null
  231. }
  232. ##
  233. ## Python smtp sending
  234. ##
  235. # Send the message via Python using SMTP_* variables
  236. _smtp_send_python() {
  237. _debug "Python version" "$("$SMTP_BIN" --version 2>&1)"
  238. # language=Python
  239. "$SMTP_BIN" <<PYTHON
  240. # This code is meant to work with either Python 2.7.x or Python 3.4+.
  241. try:
  242. try:
  243. from email.message import EmailMessage
  244. except ImportError:
  245. from email.mime.text import MIMEText as EmailMessage # Python 2
  246. from smtplib import SMTP, SMTP_SSL, SMTPException
  247. from socket import error as SocketError
  248. except ImportError as err:
  249. print("A required Python standard package is missing. This system may have"
  250. " a reduced version of Python unsuitable for sending mail: %s" % err)
  251. exit(1)
  252. show_transcript = """$SMTP_SHOW_TRANSCRIPT""" == "True"
  253. smtp_host = """$SMTP_HOST"""
  254. smtp_port = int("""$SMTP_PORT""")
  255. smtp_secure = """$SMTP_SECURE"""
  256. username = """$SMTP_USERNAME"""
  257. password = """$SMTP_PASSWORD"""
  258. timeout=int("""$SMTP_TIMEOUT""") # seconds
  259. x_mailer="""$SMTP_X_MAILER"""
  260. from_email="""$SMTP_FROM"""
  261. to_emails="""$SMTP_TO""" # can be comma-separated
  262. subject="""$SMTP_SUBJECT"""
  263. content="""$SMTP_CONTENT"""
  264. try:
  265. msg = EmailMessage()
  266. msg.set_content(content)
  267. except (AttributeError, TypeError):
  268. # Python 2 MIMEText
  269. msg = EmailMessage(content)
  270. msg["Subject"] = subject
  271. msg["From"] = from_email
  272. msg["To"] = to_emails
  273. msg["X-Mailer"] = x_mailer
  274. smtp = None
  275. try:
  276. if smtp_secure == "ssl":
  277. smtp = SMTP_SSL(smtp_host, smtp_port, timeout=timeout)
  278. else:
  279. smtp = SMTP(smtp_host, smtp_port, timeout=timeout)
  280. smtp.set_debuglevel(show_transcript)
  281. if smtp_secure == "tls":
  282. smtp.starttls()
  283. if username or password:
  284. smtp.login(username, password)
  285. smtp.sendmail(msg["From"], msg["To"].split(","), msg.as_string())
  286. except SMTPException as err:
  287. # Output just the error (skip the Python stack trace) for SMTP errors
  288. print("Error sending: %r" % err)
  289. exit(1)
  290. except SocketError as err:
  291. print("Error connecting to %s:%d: %r" % (smtp_host, smtp_port, err))
  292. exit(1)
  293. finally:
  294. if smtp is not None:
  295. smtp.quit()
  296. PYTHON
  297. }
  298. ##
  299. ## Conf helpers
  300. ##
  301. #_readaccountconf_mutable_default name default_value
  302. # Given a name like MY_CONF:
  303. # - if MY_CONF is set and non-empty, output $MY_CONF
  304. # - if MY_CONF is set _empty_, output $default_value
  305. # (lets user `export MY_CONF=` to clear previous saved value
  306. # and return to default, without user having to know default)
  307. # - otherwise if _readaccountconf_mutable $name is non-empty, return that
  308. # (value of SAVED_MY_CONF from account.conf)
  309. # - otherwise output $default_value
  310. _readaccountconf_mutable_default() {
  311. _name="$1"
  312. _default_value="$2"
  313. eval "_value=\"\$$_name\""
  314. eval "_explicit_empty_value=\"\${${_name}+empty}\""
  315. if [ -z "${_value}" ] && [ "${_explicit_empty_value:-}" != "empty" ]; then
  316. _value="$(_readaccountconf_mutable "$_name")"
  317. fi
  318. if [ -z "${_value}" ]; then
  319. _value="$_default_value"
  320. fi
  321. printf "%s" "$_value"
  322. }
  323. #_saveaccountconf_mutable_default name value default_value base64encode
  324. # Like _saveaccountconf_mutable, but if value is default_value
  325. # then _clearaccountconf_mutable instead
  326. _saveaccountconf_mutable_default() {
  327. _name="$1"
  328. _value="$2"
  329. _default_value="$3"
  330. _base64encode="$4"
  331. if [ "$_value" != "$_default_value" ]; then
  332. _saveaccountconf_mutable "$_name" "$_value" "$_base64encode"
  333. else
  334. _clearaccountconf_mutable "$_name"
  335. fi
  336. }