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