# SMTP_TO="to@example.com" # required
# SMTP_HOST="smtp.example.com" # required
# SMTP_PORT="25" # defaults to 25, 465 or 587 depending on SMTP_SECURE
-# SMTP_SECURE="none" # one of "none", "ssl" (implicit TLS, TLS Wrapper), "tls" (explicit TLS, STARTTLS)
+# SMTP_SECURE="tls" # one of "none", "ssl" (implicit TLS, TLS Wrapper), "tls" (explicit TLS, STARTTLS)
# SMTP_USERNAME="" # set if SMTP server requires login
# SMTP_PASSWORD="" # set if SMTP server requires login
# SMTP_TIMEOUT="30" # seconds for SMTP operations to timeout
-# SMTP_BIN="/path/to/curl_or_python" # default finds first of curl, python3, or python on PATH
+# SMTP_BIN="/path/to/python_or_curl" # default finds first of python3, python2.7, python, pypy3, pypy, curl on PATH
-SMTP_SECURE_DEFAULT="none"
+SMTP_SECURE_DEFAULT="tls"
SMTP_TIMEOUT_DEFAULT="30"
# subject content statuscode
# Look for a command that can communicate with an SMTP server.
# (Please don't add sendmail, ssmtp, mutt, mail, or msmtp here.
# Those are already handled by the "mail" notify hook.)
- for cmd in curl python3 python2.7 python pypy3 pypy; do
+ for cmd in python3 python2.7 python pypy3 pypy curl; do
if _exists "$cmd"; then
SMTP_BIN="$cmd"
break
_saveaccountconf_mutable_default SMTP_BIN "$SMTP_BIN"
SMTP_FROM="$(_readaccountconf_mutable_default SMTP_FROM)"
+ SMTP_FROM="$(_clean_email_header "$SMTP_FROM")"
if [ -z "$SMTP_FROM" ]; then
_err "You must define SMTP_FROM as the sender email address."
return 1
fi
+ if _email_has_display_name "$SMTP_FROM"; then
+ _err "SMTP_FROM must be only a simple email address (sender@example.com)."
+ _err "Change your SMTP_FROM='$SMTP_FROM' to remove the display name."
+ return 1
+ fi
_debug SMTP_FROM "$SMTP_FROM"
_saveaccountconf_mutable_default SMTP_FROM "$SMTP_FROM"
SMTP_TO="$(_readaccountconf_mutable_default SMTP_TO)"
+ SMTP_TO="$(_clean_email_header "$SMTP_TO")"
if [ -z "$SMTP_TO" ]; then
- _err "You must define SMTP_TO as the recipient email address."
+ _err "You must define SMTP_TO as the recipient email address(es)."
+ return 1
+ fi
+ if _email_has_display_name "$SMTP_TO"; then
+ _err "SMTP_TO must be only simple email addresses (to@example.com,to2@example.com)."
+ _err "Change your SMTP_TO='$SMTP_TO' to remove the display name(s)."
return 1
fi
_debug SMTP_TO "$SMTP_TO"
_debug SMTP_TIMEOUT "$SMTP_TIMEOUT"
_saveaccountconf_mutable_default SMTP_TIMEOUT "$SMTP_TIMEOUT" "$SMTP_TIMEOUT_DEFAULT"
- SMTP_X_MAILER="${PROJECT_NAME} ${VER} --notify-hook smtp"
+ SMTP_X_MAILER="$(_clean_email_header "$PROJECT_NAME $VER --notify-hook smtp")"
# Run with --debug 2 (or above) to echo the transcript of the SMTP session.
# Careful: this may include SMTP_PASSWORD in plaintext!
SMTP_SHOW_TRANSCRIPT=""
fi
+ SMTP_SUBJECT=$(_clean_email_header "$SMTP_SUBJECT")
_debug SMTP_SUBJECT "$SMTP_SUBJECT"
_debug SMTP_CONTENT "$SMTP_CONTENT"
return 0
}
+# Strip CR and NL from text to prevent MIME header injection
+# text
+_clean_email_header() {
+ printf "%s" "$(echo "$1" | tr -d "\r\n")"
+}
+
+# Simple check for display name in an email address (< > or ")
+# email
+_email_has_display_name() {
+ _email="$1"
+ echo "$_email" | grep -q -E '^.*[<>"]'
+}
+
##
## curl smtp sending
##
# Send the message via curl using SMTP_* variables
_smtp_send_curl() {
- # curl passes --mail-from and --mail-rcpt directly to the SMTP protocol without
- # additional parsing, and SMTP requires addr-spec only (no display names).
- # In the future, maybe try to parse the addr-spec out for curl args (non-trivial).
- if _email_has_display_name "$SMTP_FROM"; then
- _err "curl smtp only allows a simple email address in SMTP_FROM."
- _err "Change your SMTP_FROM='$SMTP_FROM' to remove the display name."
- return 1
- fi
- if _email_has_display_name "$SMTP_TO"; then
- _err "curl smtp only allows simple email addresses in SMTP_TO."
- _err "Change your SMTP_TO='$SMTP_TO' to remove the display name(s)."
- return 1
- fi
-
# Build curl args in $@
-
case "$SMTP_SECURE" in
none)
set -- --url "smtp://${SMTP_HOST}:${SMTP_PORT}"
echo "$raw_message" | "$SMTP_BIN" "$@"
}
-# Output an RFC-822 / RFC-5322 email message using SMTP_* variables
+# Output an RFC-822 / RFC-5322 email message using SMTP_* variables.
+# (This assumes variables have already been cleaned for use in email headers.)
_smtp_raw_message() {
echo "From: $SMTP_FROM"
echo "To: $SMTP_TO"
_text="$1"
# (regex character ranges like [a-z] can be locale-dependent; enumerate ASCII chars to avoid that)
_ascii='] $`"'"[!#%&'()*+,./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ~^_abcdefghijklmnopqrstuvwxyz{|}~-"
- if expr "$_text" : "^.*[^$_ascii]" >/dev/null; then
+ if echo "$_text" | grep -q -E "^.*[^$_ascii]"; then
# At least one non-ASCII char; convert entire thing to encoded word
printf "%s" "=?UTF-8?B?$(printf "%s" "$_text" | _base64)?="
else
LC_TIME="$_old_lc_time"
}
-# Simple check for display name in an email address (< > or ")
-# email
-_email_has_display_name() {
- _email="$1"
- expr "$_email" : '^.*[<>"]' >/dev/null
-}
-
##
## Python smtp sending
##
try:
try:
from email.message import EmailMessage
+ from email.policy import default as email_policy_default
except ImportError:
- from email.mime.text import MIMEText as EmailMessage # Python 2
+ # Python 2 (or < 3.3)
+ from email.mime.text import MIMEText as EmailMessage
+ email_policy_default = None
+ from email.utils import formatdate as rfc2822_date
from smtplib import SMTP, SMTP_SSL, SMTPException
from socket import error as SocketError
except ImportError as err:
content="""$SMTP_CONTENT"""
try:
- msg = EmailMessage()
+ msg = EmailMessage(policy=email_policy_default)
msg.set_content(content)
except (AttributeError, TypeError):
# Python 2 MIMEText
msg["Subject"] = subject
msg["From"] = from_email
msg["To"] = to_emails
+msg["Date"] = rfc2822_date(localtime=True)
msg["X-Mailer"] = x_mailer
smtp = None
# - if MY_CONF is set _empty_, output $default_value
# (lets user `export MY_CONF=` to clear previous saved value
# and return to default, without user having to know default)
-# - otherwise if _readaccountconf_mutable $name is non-empty, return that
+# - otherwise if _readaccountconf_mutable MY_CONF is non-empty, return that
# (value of SAVED_MY_CONF from account.conf)
# - otherwise output $default_value
_readaccountconf_mutable_default() {
_default_value="$2"
eval "_value=\"\$$_name\""
- eval "_explicit_empty_value=\"\${${_name}+empty}\""
- if [ -z "${_value}" ] && [ "${_explicit_empty_value:-}" != "empty" ]; then
+ eval "_name_is_set=\"\${${_name}+true}\""
+ # ($_name_is_set is "true" if $$_name is set to anything, including empty)
+ if [ -z "${_value}" ] && [ "${_name_is_set:-}" != "true" ]; then
_value="$(_readaccountconf_mutable "$_name")"
fi
if [ -z "${_value}" ]; then