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.

377 lines
11 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. if _exists date; then
  195. echo "Date: $(date +'%a, %-d %b %Y %H:%M:%S %z')"
  196. fi
  197. echo "Content-Type: text/plain; charset=utf-8"
  198. echo "X-Mailer: $SMTP_X_MAILER"
  199. echo
  200. echo "$SMTP_CONTENT"
  201. }
  202. # Convert text to RFC-2047 MIME "encoded word" format if it contains non-ASCII chars
  203. # text
  204. _mime_encoded_word() {
  205. _text="$1"
  206. # (regex character ranges like [a-z] can be locale-dependent; enumerate ASCII chars to avoid that)
  207. _ascii='] $`"'"[!#%&'()*+,./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ~^_abcdefghijklmnopqrstuvwxyz{|}~-"
  208. if expr "$_text" : "^.*[^$_ascii]" >/dev/null; then
  209. # At least one non-ASCII char; convert entire thing to encoded word
  210. printf "%s" "=?UTF-8?B?$(printf "%s" "$_text" | _base64)?="
  211. else
  212. # Just printable ASCII, no conversion needed
  213. printf "%s" "$_text"
  214. fi
  215. }
  216. # Simple check for display name in an email address (< > or ")
  217. # email
  218. _email_has_display_name() {
  219. _email="$1"
  220. expr "$_email" : '^.*[<>"]' >/dev/null
  221. }
  222. ##
  223. ## Python smtp sending
  224. ##
  225. # Send the message via Python using SMTP_* variables
  226. _smtp_send_python() {
  227. _debug "Python version" "$("$SMTP_BIN" --version 2>&1)"
  228. # language=Python
  229. "$SMTP_BIN" <<PYTHON
  230. # This code is meant to work with either Python 2.7.x or Python 3.4+.
  231. try:
  232. try:
  233. from email.message import EmailMessage
  234. except ImportError:
  235. from email.mime.text import MIMEText as EmailMessage # Python 2
  236. from smtplib import SMTP, SMTP_SSL, SMTPException
  237. from socket import error as SocketError
  238. except ImportError as err:
  239. print("A required Python standard package is missing. This system may have"
  240. " a reduced version of Python unsuitable for sending mail: %s" % err)
  241. exit(1)
  242. show_transcript = """$SMTP_SHOW_TRANSCRIPT""" == "True"
  243. smtp_host = """$SMTP_HOST"""
  244. smtp_port = int("""$SMTP_PORT""")
  245. smtp_secure = """$SMTP_SECURE"""
  246. username = """$SMTP_USERNAME"""
  247. password = """$SMTP_PASSWORD"""
  248. timeout=int("""$SMTP_TIMEOUT""") # seconds
  249. x_mailer="""$SMTP_X_MAILER"""
  250. from_email="""$SMTP_FROM"""
  251. to_emails="""$SMTP_TO""" # can be comma-separated
  252. subject="""$SMTP_SUBJECT"""
  253. content="""$SMTP_CONTENT"""
  254. try:
  255. msg = EmailMessage()
  256. msg.set_content(content)
  257. except (AttributeError, TypeError):
  258. # Python 2 MIMEText
  259. msg = EmailMessage(content)
  260. msg["Subject"] = subject
  261. msg["From"] = from_email
  262. msg["To"] = to_emails
  263. msg["X-Mailer"] = x_mailer
  264. smtp = None
  265. try:
  266. if smtp_secure == "ssl":
  267. smtp = SMTP_SSL(smtp_host, smtp_port, timeout=timeout)
  268. else:
  269. smtp = SMTP(smtp_host, smtp_port, timeout=timeout)
  270. smtp.set_debuglevel(show_transcript)
  271. if smtp_secure == "tls":
  272. smtp.starttls()
  273. if username or password:
  274. smtp.login(username, password)
  275. smtp.sendmail(msg["From"], msg["To"].split(","), msg.as_string())
  276. except SMTPException as err:
  277. # Output just the error (skip the Python stack trace) for SMTP errors
  278. print("Error sending: %r" % err)
  279. exit(1)
  280. except SocketError as err:
  281. print("Error connecting to %s:%d: %r" % (smtp_host, smtp_port, err))
  282. exit(1)
  283. finally:
  284. if smtp is not None:
  285. smtp.quit()
  286. PYTHON
  287. }
  288. ##
  289. ## Conf helpers
  290. ##
  291. #_readaccountconf_mutable_default name default_value
  292. # Given a name like MY_CONF:
  293. # - if MY_CONF is set and non-empty, output $MY_CONF
  294. # - if MY_CONF is set _empty_, output $default_value
  295. # (lets user `export MY_CONF=` to clear previous saved value
  296. # and return to default, without user having to know default)
  297. # - otherwise if _readaccountconf_mutable $name is non-empty, return that
  298. # (value of SAVED_MY_CONF from account.conf)
  299. # - otherwise output $default_value
  300. _readaccountconf_mutable_default() {
  301. _name="$1"
  302. _default_value="$2"
  303. eval "_value=\"\$$_name\""
  304. eval "_explicit_empty_value=\"\${${_name}+empty}\""
  305. if [ -z "${_value}" ] && [ "${_explicit_empty_value:-}" != "empty" ]; then
  306. _value="$(_readaccountconf_mutable "$_name")"
  307. fi
  308. if [ -z "${_value}" ]; then
  309. _value="$_default_value"
  310. fi
  311. printf "%s" "$_value"
  312. }
  313. #_saveaccountconf_mutable_default name value default_value base64encode
  314. # Like _saveaccountconf_mutable, but if value is default_value
  315. # then _clearaccountconf_mutable instead
  316. _saveaccountconf_mutable_default() {
  317. _name="$1"
  318. _value="$2"
  319. _default_value="$3"
  320. _base64encode="$4"
  321. if [ "$_value" != "$_default_value" ]; then
  322. _saveaccountconf_mutable "$_name" "$_value" "$_base64encode"
  323. else
  324. _clearaccountconf_mutable "$_name"
  325. fi
  326. }