smtplib.SMTP API calls such as .sendmail() or .login() make sure on their first line that this command is sent, by calling .ehlo_or_helo_if_needed(). However, test_smtp_connection() does not use SMTP.sendmail() to simulate sending email, or the email would really be sent. Instead it uses the low-level API SMTP.mail() which does not make sure that HELO was sent. The connection test could be fixed by ensuring that this command was sent to the server in the test method. However, this could lead to inconsistent cases where the connection test passes whereas another usage in the code fails because the command was not sent. For example, someone could use the low-level API for some reason. EHLO could have already been sent because if authentication is enabled smtplib.SMTP.login() calls ehlo_or_helo_if_needed(). Therefore it is correct to call it ourselves at the end of our connect() method. STARTTLS sends a first EHLO, negociates the encryption, and leaves the state without the second EHLO, which should still be sent to the server, as stated by RFC 3207, in section "4.2 Result of the STARTTLS Command". https://www.ietf.org/rfc/rfc3207.txt closes odoo/odoo#34550 Signed-off-by: Julien Legros (jle) <jle@odoo.com>
519 lines
26 KiB
Python
519 lines
26 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
from email import encoders
|
|
from email.charset import Charset
|
|
from email.header import Header
|
|
from email.mime.base import MIMEBase
|
|
from email.mime.multipart import MIMEMultipart
|
|
from email.mime.text import MIMEText
|
|
from email.utils import COMMASPACE, formataddr, formatdate, getaddresses, make_msgid
|
|
import logging
|
|
import re
|
|
import smtplib
|
|
import threading
|
|
|
|
import html2text
|
|
|
|
from odoo import api, fields, models, tools, _
|
|
from odoo.exceptions import except_orm, UserError
|
|
from odoo.tools import ustr, pycompat
|
|
|
|
_logger = logging.getLogger(__name__)
|
|
_test_logger = logging.getLogger('odoo.tests')
|
|
|
|
SMTP_TIMEOUT = 60
|
|
|
|
|
|
class MailDeliveryException(except_orm):
|
|
"""Specific exception subclass for mail delivery errors"""
|
|
def __init__(self, name, value):
|
|
super(MailDeliveryException, self).__init__(name, value)
|
|
|
|
# Python 3: patch SMTP's internal printer/debugger
|
|
def _print_debug(self, *args):
|
|
_logger.debug(' '.join(str(a) for a in args))
|
|
smtplib.SMTP._print_debug = _print_debug
|
|
|
|
# Python 2: replace smtplib's stderr
|
|
class WriteToLogger(object):
|
|
def write(self, s):
|
|
_logger.debug(s)
|
|
smtplib.stderr = WriteToLogger()
|
|
|
|
def is_ascii(s):
|
|
return all(ord(cp) < 128 for cp in s)
|
|
|
|
def encode_header(header_text):
|
|
"""Returns an appropriate representation of the given header value,
|
|
suitable for direct assignment as a header value in an
|
|
email.message.Message. RFC2822 assumes that headers contain
|
|
only 7-bit characters, so we ensure it is the case, using
|
|
RFC2047 encoding when needed.
|
|
|
|
:param header_text: unicode or utf-8 encoded string with header value
|
|
:rtype: string | email.header.Header
|
|
:return: if ``header_text`` represents a plain ASCII string,
|
|
return the same 7-bit string, otherwise returns an email.header.Header
|
|
that will perform the appropriate RFC2047 encoding of
|
|
non-ASCII values.
|
|
"""
|
|
if not header_text:
|
|
return ""
|
|
header_text = ustr(header_text) # FIXME: require unicode higher up?
|
|
if is_ascii(header_text):
|
|
return pycompat.to_native(header_text)
|
|
return Header(header_text, 'utf-8')
|
|
|
|
def encode_header_param(param_text):
|
|
"""Returns an appropriate RFC2047 encoded representation of the given
|
|
header parameter value, suitable for direct assignation as the
|
|
param value (e.g. via Message.set_param() or Message.add_header())
|
|
RFC2822 assumes that headers contain only 7-bit characters,
|
|
so we ensure it is the case, using RFC2047 encoding when needed.
|
|
|
|
:param param_text: unicode or utf-8 encoded string with header value
|
|
:rtype: string
|
|
:return: if ``param_text`` represents a plain ASCII string,
|
|
return the same 7-bit string, otherwise returns an
|
|
ASCII string containing the RFC2047 encoded text.
|
|
"""
|
|
# For details see the encode_header() method that uses the same logic
|
|
if not param_text:
|
|
return ""
|
|
param_text = ustr(param_text) # FIXME: require unicode higher up?
|
|
if is_ascii(param_text):
|
|
return pycompat.to_native(param_text) # TODO: is that actually necessary?
|
|
return Charset("utf-8").header_encode(param_text)
|
|
|
|
address_pattern = re.compile(r'([^ ,<@]+@[^> ,]+)')
|
|
|
|
def extract_rfc2822_addresses(text):
|
|
"""Returns a list of valid RFC2822 addresses
|
|
that can be found in ``source``, ignoring
|
|
malformed ones and non-ASCII ones.
|
|
"""
|
|
if not text:
|
|
return []
|
|
candidates = address_pattern.findall(ustr(text))
|
|
return [c for c in candidates if is_ascii(c)]
|
|
|
|
|
|
def encode_rfc2822_address_header(header_text):
|
|
"""If ``header_text`` contains non-ASCII characters,
|
|
attempts to locate patterns of the form
|
|
``"Name" <address@domain>`` and replace the
|
|
``"Name"`` portion by the RFC2047-encoded
|
|
version, preserving the address part untouched.
|
|
"""
|
|
def encode_addr(addr):
|
|
name, email = addr
|
|
# If s is a <text string>, then charset is a hint specifying the
|
|
# character set of the characters in the string. The Unicode string
|
|
# will be encoded using the following charsets in order: us-ascii,
|
|
# the charset hint, utf-8. The first character set to not provoke a
|
|
# UnicodeError is used.
|
|
# -> always pass a text string to Header
|
|
|
|
# also Header.__str__ in Python 3 "Returns an approximation of the
|
|
# Header as a string, using an unlimited line length.", the old one
|
|
# was "A synonym for Header.encode()." so call encode() directly?
|
|
name = Header(pycompat.to_text(name)).encode()
|
|
# if the from does not follow the (name <addr>),* convention, we might
|
|
# try to encode meaningless strings as address, as getaddresses is naive
|
|
# note it would also fail on real addresses with non-ascii characters
|
|
try:
|
|
return formataddr((name, email))
|
|
except UnicodeEncodeError:
|
|
_logger.warning(_('Failed to encode the address %s\n'
|
|
'from mail header:\n%s') % addr, header_text)
|
|
return ""
|
|
|
|
addresses = getaddresses([pycompat.to_native(ustr(header_text))])
|
|
return COMMASPACE.join(a for a in (encode_addr(addr) for addr in addresses) if a)
|
|
|
|
|
|
class IrMailServer(models.Model):
|
|
"""Represents an SMTP server, able to send outgoing emails, with SSL and TLS capabilities."""
|
|
_name = "ir.mail_server"
|
|
|
|
NO_VALID_RECIPIENT = ("At least one valid recipient address should be "
|
|
"specified for outgoing emails (To/Cc/Bcc)")
|
|
|
|
name = fields.Char(string='Description', required=True, index=True)
|
|
smtp_host = fields.Char(string='SMTP Server', required=True, help="Hostname or IP of SMTP server")
|
|
smtp_port = fields.Integer(string='SMTP Port', size=5, required=True, default=25, help="SMTP Port. Usually 465 for SSL, and 25 or 587 for other cases.")
|
|
smtp_user = fields.Char(string='Username', help="Optional username for SMTP authentication", groups='base.group_system')
|
|
smtp_pass = fields.Char(string='Password', help="Optional password for SMTP authentication", groups='base.group_system')
|
|
smtp_encryption = fields.Selection([('none', 'None'),
|
|
('starttls', 'TLS (STARTTLS)'),
|
|
('ssl', 'SSL/TLS')],
|
|
string='Connection Security', required=True, default='none',
|
|
help="Choose the connection encryption scheme:\n"
|
|
"- None: SMTP sessions are done in cleartext.\n"
|
|
"- TLS (STARTTLS): TLS encryption is requested at start of SMTP session (Recommended)\n"
|
|
"- SSL/TLS: SMTP sessions are encrypted with SSL/TLS through a dedicated port (default: 465)")
|
|
smtp_debug = fields.Boolean(string='Debugging', help="If enabled, the full output of SMTP sessions will "
|
|
"be written to the server log at DEBUG level "
|
|
"(this is very verbose and may include confidential info!)")
|
|
sequence = fields.Integer(string='Priority', default=10, help="When no specific mail server is requested for a mail, the highest priority one "
|
|
"is used. Default priority is 10 (smaller number = higher priority)")
|
|
active = fields.Boolean(default=True)
|
|
|
|
@api.multi
|
|
def test_smtp_connection(self):
|
|
for server in self:
|
|
smtp = False
|
|
try:
|
|
smtp = self.connect(mail_server_id=server.id)
|
|
# simulate sending an email from current user's address - without sending it!
|
|
email_from, email_to = self.env.user.email, 'noreply@odoo.com'
|
|
if not email_from:
|
|
raise UserError(_('Please configure an email on the current user to simulate '
|
|
'sending an email message via this outgoing server'))
|
|
# Testing the MAIL FROM step should detect sender filter problems
|
|
(code, repl) = smtp.mail(email_from)
|
|
if code != 250:
|
|
raise UserError(_('The server refused the sender address (%(email_from)s) '
|
|
'with error %(repl)s') % locals())
|
|
# Testing the RCPT TO step should detect most relaying problems
|
|
(code, repl) = smtp.rcpt(email_to)
|
|
if code not in (250, 251):
|
|
raise UserError(_('The server refused the test recipient (%(email_to)s) '
|
|
'with error %(repl)s') % locals())
|
|
# Beginning the DATA step should detect some deferred rejections
|
|
# Can't use self.data() as it would actually send the mail!
|
|
smtp.putcmd("data")
|
|
(code, repl) = smtp.getreply()
|
|
if code != 354:
|
|
raise UserError(_('The server refused the test connection '
|
|
'with error %(repl)s') % locals())
|
|
except UserError as e:
|
|
# let UserErrors (messages) bubble up
|
|
raise e
|
|
except Exception as e:
|
|
raise UserError(_("Connection Test Failed! Here is what we got instead:\n %s") % ustr(e))
|
|
finally:
|
|
try:
|
|
if smtp:
|
|
smtp.close()
|
|
except Exception:
|
|
# ignored, just a consequence of the previous exception
|
|
pass
|
|
raise UserError(_("Connection Test Succeeded! Everything seems properly set up!"))
|
|
|
|
def connect(self, host=None, port=None, user=None, password=None, encryption=None,
|
|
smtp_debug=False, mail_server_id=None):
|
|
"""Returns a new SMTP connection to the given SMTP server.
|
|
When running in test mode, this method does nothing and returns `None`.
|
|
|
|
:param host: host or IP of SMTP server to connect to, if mail_server_id not passed
|
|
:param int port: SMTP port to connect to
|
|
:param user: optional username to authenticate with
|
|
:param password: optional password to authenticate with
|
|
:param string encryption: optional, ``'ssl'`` | ``'starttls'``
|
|
:param bool smtp_debug: toggle debugging of SMTP sessions (all i/o
|
|
will be output in logs)
|
|
:param mail_server_id: ID of specific mail server to use (overrides other parameters)
|
|
"""
|
|
# Do not actually connect while running in test mode
|
|
if getattr(threading.currentThread(), 'testing', False):
|
|
return None
|
|
|
|
mail_server = smtp_encryption = None
|
|
if mail_server_id:
|
|
mail_server = self.sudo().browse(mail_server_id)
|
|
elif not host:
|
|
mail_server = self.sudo().search([], order='sequence', limit=1)
|
|
|
|
if mail_server:
|
|
smtp_server = mail_server.smtp_host
|
|
smtp_port = mail_server.smtp_port
|
|
smtp_user = mail_server.smtp_user
|
|
smtp_password = mail_server.smtp_pass
|
|
smtp_encryption = mail_server.smtp_encryption
|
|
smtp_debug = smtp_debug or mail_server.smtp_debug
|
|
else:
|
|
# we were passed individual smtp parameters or nothing and there is no default server
|
|
smtp_server = host or tools.config.get('smtp_server')
|
|
smtp_port = tools.config.get('smtp_port', 25) if port is None else port
|
|
smtp_user = user or tools.config.get('smtp_user')
|
|
smtp_password = password or tools.config.get('smtp_password')
|
|
smtp_encryption = encryption
|
|
if smtp_encryption is None and tools.config.get('smtp_ssl'):
|
|
smtp_encryption = 'starttls' # smtp_ssl => STARTTLS as of v7
|
|
|
|
if not smtp_server:
|
|
raise UserError(
|
|
(_("Missing SMTP Server") + "\n" +
|
|
_("Please define at least one SMTP server, "
|
|
"or provide the SMTP parameters explicitly.")))
|
|
|
|
if smtp_encryption == 'ssl':
|
|
if 'SMTP_SSL' not in smtplib.__all__:
|
|
raise UserError(
|
|
_("Your Odoo Server does not support SMTP-over-SSL. "
|
|
"You could use STARTTLS instead. "
|
|
"If SSL is needed, an upgrade to Python 2.6 on the server-side "
|
|
"should do the trick."))
|
|
connection = smtplib.SMTP_SSL(smtp_server, smtp_port, timeout=SMTP_TIMEOUT)
|
|
else:
|
|
connection = smtplib.SMTP(smtp_server, smtp_port, timeout=SMTP_TIMEOUT)
|
|
connection.set_debuglevel(smtp_debug)
|
|
if smtp_encryption == 'starttls':
|
|
# starttls() will perform ehlo() if needed first
|
|
# and will discard the previous list of services
|
|
# after successfully performing STARTTLS command,
|
|
# (as per RFC 3207) so for example any AUTH
|
|
# capability that appears only on encrypted channels
|
|
# will be correctly detected for next step
|
|
connection.starttls()
|
|
|
|
if smtp_user:
|
|
# Attempt authentication - will raise if AUTH service not supported
|
|
# The user/password must be converted to bytestrings in order to be usable for
|
|
# certain hashing schemes, like HMAC.
|
|
# See also bug #597143 and python issue #5285
|
|
smtp_user = pycompat.to_native(ustr(smtp_user))
|
|
smtp_password = pycompat.to_native(ustr(smtp_password))
|
|
connection.login(smtp_user, smtp_password)
|
|
|
|
# Some methods of SMTP don't check whether EHLO/HELO was sent.
|
|
# Anyway, as it may have been sent by login(), all subsequent usages should consider this command as sent.
|
|
connection.ehlo_or_helo_if_needed()
|
|
|
|
return connection
|
|
|
|
def build_email(self, email_from, email_to, subject, body, email_cc=None, email_bcc=None, reply_to=False,
|
|
attachments=None, message_id=None, references=None, object_id=False, subtype='plain', headers=None,
|
|
body_alternative=None, subtype_alternative='plain'):
|
|
"""Constructs an RFC2822 email.message.Message object based on the keyword arguments passed, and returns it.
|
|
|
|
:param string email_from: sender email address
|
|
:param list email_to: list of recipient addresses (to be joined with commas)
|
|
:param string subject: email subject (no pre-encoding/quoting necessary)
|
|
:param string body: email body, of the type ``subtype`` (by default, plaintext).
|
|
If html subtype is used, the message will be automatically converted
|
|
to plaintext and wrapped in multipart/alternative, unless an explicit
|
|
``body_alternative`` version is passed.
|
|
:param string body_alternative: optional alternative body, of the type specified in ``subtype_alternative``
|
|
:param string reply_to: optional value of Reply-To header
|
|
:param string object_id: optional tracking identifier, to be included in the message-id for
|
|
recognizing replies. Suggested format for object-id is "res_id-model",
|
|
e.g. "12345-crm.lead".
|
|
:param string subtype: optional mime subtype for the text body (usually 'plain' or 'html'),
|
|
must match the format of the ``body`` parameter. Default is 'plain',
|
|
making the content part of the mail "text/plain".
|
|
:param string subtype_alternative: optional mime subtype of ``body_alternative`` (usually 'plain'
|
|
or 'html'). Default is 'plain'.
|
|
:param list attachments: list of (filename, filecontents) pairs, where filecontents is a string
|
|
containing the bytes of the attachment
|
|
:param list email_cc: optional list of string values for CC header (to be joined with commas)
|
|
:param list email_bcc: optional list of string values for BCC header (to be joined with commas)
|
|
:param dict headers: optional map of headers to set on the outgoing mail (may override the
|
|
other headers, including Subject, Reply-To, Message-Id, etc.)
|
|
:rtype: email.message.Message (usually MIMEMultipart)
|
|
:return: the new RFC2822 email message
|
|
"""
|
|
email_from = email_from or tools.config.get('email_from')
|
|
assert email_from, "You must either provide a sender address explicitly or configure "\
|
|
"a global sender address in the server configuration or with the "\
|
|
"--email-from startup parameter."
|
|
|
|
# Note: we must force all strings to to 8-bit utf-8 when crafting message,
|
|
# or use encode_header() for headers, which does it automatically.
|
|
|
|
headers = headers or {} # need valid dict later
|
|
email_cc = email_cc or []
|
|
email_bcc = email_bcc or []
|
|
body = body or u''
|
|
|
|
email_body = ustr(body)
|
|
email_text_part = MIMEText(email_body, _subtype=subtype, _charset='utf-8')
|
|
msg = MIMEMultipart()
|
|
|
|
if not message_id:
|
|
if object_id:
|
|
message_id = tools.generate_tracking_message_id(object_id)
|
|
else:
|
|
message_id = make_msgid()
|
|
msg['Message-Id'] = encode_header(message_id)
|
|
if references:
|
|
msg['references'] = encode_header(references)
|
|
msg['Subject'] = encode_header(subject)
|
|
msg['From'] = encode_rfc2822_address_header(email_from)
|
|
del msg['Reply-To']
|
|
if reply_to:
|
|
msg['Reply-To'] = encode_rfc2822_address_header(reply_to)
|
|
else:
|
|
msg['Reply-To'] = msg['From']
|
|
msg['To'] = encode_rfc2822_address_header(COMMASPACE.join(email_to))
|
|
if email_cc:
|
|
msg['Cc'] = encode_rfc2822_address_header(COMMASPACE.join(email_cc))
|
|
if email_bcc:
|
|
msg['Bcc'] = encode_rfc2822_address_header(COMMASPACE.join(email_bcc))
|
|
msg['Date'] = formatdate()
|
|
# Custom headers may override normal headers or provide additional ones
|
|
for key, value in headers.items():
|
|
msg[pycompat.to_native(ustr(key))] = encode_header(value)
|
|
|
|
if subtype == 'html' and not body_alternative:
|
|
# Always provide alternative text body ourselves if possible.
|
|
text = html2text.html2text(email_body)
|
|
alternative_part = MIMEMultipart(_subtype="alternative")
|
|
alternative_part.attach(MIMEText(text, _charset='utf-8', _subtype='plain'))
|
|
alternative_part.attach(email_text_part)
|
|
msg.attach(alternative_part)
|
|
elif body_alternative:
|
|
# Include both alternatives, as specified, within a multipart/alternative part
|
|
alternative_part = MIMEMultipart(_subtype="alternative")
|
|
body_alternative_ = ustr(body_alternative)
|
|
alternative_body_part = MIMEText(body_alternative_, _subtype=subtype_alternative, _charset='utf-8')
|
|
alternative_part.attach(alternative_body_part)
|
|
alternative_part.attach(email_text_part)
|
|
msg.attach(alternative_part)
|
|
else:
|
|
msg.attach(email_text_part)
|
|
|
|
if attachments:
|
|
for (fname, fcontent, mime) in attachments:
|
|
filename_rfc2047 = encode_header_param(fname)
|
|
if mime and '/' in mime:
|
|
maintype, subtype = mime.split('/', 1)
|
|
part = MIMEBase(maintype, subtype)
|
|
else:
|
|
part = MIMEBase('application', "octet-stream")
|
|
|
|
# The default RFC2231 encoding of Message.add_header() works in Thunderbird but not GMail
|
|
# so we fix it by using RFC2047 encoding for the filename instead.
|
|
part.set_param('name', filename_rfc2047)
|
|
part.add_header('Content-Disposition', 'attachment', filename=filename_rfc2047)
|
|
|
|
part.set_payload(fcontent)
|
|
encoders.encode_base64(part)
|
|
msg.attach(part)
|
|
return msg
|
|
|
|
@api.model
|
|
def _get_default_bounce_address(self):
|
|
'''Compute the default bounce address.
|
|
|
|
The default bounce address is used to set the envelop address if no
|
|
envelop address is provided in the message. It is formed by properly
|
|
joining the parameters "mail.bounce.alias" and
|
|
"mail.catchall.domain".
|
|
|
|
If "mail.bounce.alias" is not set it defaults to "postmaster-odoo".
|
|
|
|
If "mail.catchall.domain" is not set, return None.
|
|
|
|
'''
|
|
get_param = self.env['ir.config_parameter'].sudo().get_param
|
|
postmaster = get_param('mail.bounce.alias', default='postmaster-odoo')
|
|
domain = get_param('mail.catchall.domain')
|
|
if postmaster and domain:
|
|
return '%s@%s' % (postmaster, domain)
|
|
|
|
@api.model
|
|
def send_email(self, message, mail_server_id=None, smtp_server=None, smtp_port=None,
|
|
smtp_user=None, smtp_password=None, smtp_encryption=None, smtp_debug=False,
|
|
smtp_session=None):
|
|
"""Sends an email directly (no queuing).
|
|
|
|
No retries are done, the caller should handle MailDeliveryException in order to ensure that
|
|
the mail is never lost.
|
|
|
|
If the mail_server_id is provided, sends using this mail server, ignoring other smtp_* arguments.
|
|
If mail_server_id is None and smtp_server is None, use the default mail server (highest priority).
|
|
If mail_server_id is None and smtp_server is not None, use the provided smtp_* arguments.
|
|
If both mail_server_id and smtp_server are None, look for an 'smtp_server' value in server config,
|
|
and fails if not found.
|
|
|
|
:param message: the email.message.Message to send. The envelope sender will be extracted from the
|
|
``Return-Path`` (if present), or will be set to the default bounce address.
|
|
The envelope recipients will be extracted from the combined list of ``To``,
|
|
``CC`` and ``BCC`` headers.
|
|
:param smtp_session: optional pre-established SMTP session. When provided,
|
|
overrides `mail_server_id` and all the `smtp_*` parameters.
|
|
Passing the matching `mail_server_id` may yield better debugging/log
|
|
messages. The caller is in charge of disconnecting the session.
|
|
:param mail_server_id: optional id of ir.mail_server to use for sending. overrides other smtp_* arguments.
|
|
:param smtp_server: optional hostname of SMTP server to use
|
|
:param smtp_encryption: optional TLS mode, one of 'none', 'starttls' or 'ssl' (see ir.mail_server fields for explanation)
|
|
:param smtp_port: optional SMTP port, if mail_server_id is not passed
|
|
:param smtp_user: optional SMTP user, if mail_server_id is not passed
|
|
:param smtp_password: optional SMTP password to use, if mail_server_id is not passed
|
|
:param smtp_debug: optional SMTP debug flag, if mail_server_id is not passed
|
|
:return: the Message-ID of the message that was just sent, if successfully sent, otherwise raises
|
|
MailDeliveryException and logs root cause.
|
|
"""
|
|
# Use the default bounce address **only if** no Return-Path was
|
|
# provided by caller. Caller may be using Variable Envelope Return
|
|
# Path (VERP) to detect no-longer valid email addresses.
|
|
smtp_from = message['Return-Path'] or self._get_default_bounce_address() or message['From']
|
|
assert smtp_from, "The Return-Path or From header is required for any outbound email"
|
|
|
|
# The email's "Envelope From" (Return-Path), and all recipient addresses must only contain ASCII characters.
|
|
from_rfc2822 = extract_rfc2822_addresses(smtp_from)
|
|
assert from_rfc2822, ("Malformed 'Return-Path' or 'From' address: %r - "
|
|
"It should contain one valid plain ASCII email") % smtp_from
|
|
# use last extracted email, to support rarities like 'Support@MyComp <support@mycompany.com>'
|
|
smtp_from = from_rfc2822[-1]
|
|
email_to = message['To']
|
|
email_cc = message['Cc']
|
|
email_bcc = message['Bcc']
|
|
del message['Bcc']
|
|
|
|
smtp_to_list = [
|
|
address
|
|
for base in [email_to, email_cc, email_bcc]
|
|
for address in extract_rfc2822_addresses(base)
|
|
if address
|
|
]
|
|
assert smtp_to_list, self.NO_VALID_RECIPIENT
|
|
|
|
x_forge_to = message['X-Forge-To']
|
|
if x_forge_to:
|
|
# `To:` header forged, e.g. for posting on mail.channels, to avoid confusion
|
|
del message['X-Forge-To']
|
|
del message['To'] # avoid multiple To: headers!
|
|
message['To'] = x_forge_to
|
|
|
|
# Do not actually send emails in testing mode!
|
|
if getattr(threading.currentThread(), 'testing', False) or self.env.registry.in_test_mode():
|
|
_test_logger.info("skip sending email in test mode")
|
|
return message['Message-Id']
|
|
|
|
try:
|
|
message_id = message['Message-Id']
|
|
smtp = smtp_session
|
|
smtp = smtp or self.connect(
|
|
smtp_server, smtp_port, smtp_user, smtp_password,
|
|
smtp_encryption, smtp_debug, mail_server_id=mail_server_id)
|
|
smtp.sendmail(smtp_from, smtp_to_list, message.as_string())
|
|
# do not quit() a pre-established smtp_session
|
|
if not smtp_session:
|
|
smtp.quit()
|
|
except smtplib.SMTPServerDisconnected:
|
|
raise
|
|
except Exception as e:
|
|
params = (ustr(smtp_server), e.__class__.__name__, ustr(e))
|
|
msg = _("Mail delivery failed via SMTP server '%s'.\n%s: %s") % params
|
|
_logger.info(msg)
|
|
raise MailDeliveryException(_("Mail Delivery Failed"), msg)
|
|
return message_id
|
|
|
|
@api.onchange('smtp_encryption')
|
|
def _onchange_encryption(self):
|
|
result = {}
|
|
if self.smtp_encryption == 'ssl':
|
|
self.smtp_port = 465
|
|
if not 'SMTP_SSL' in smtplib.__all__:
|
|
result['warning'] = {
|
|
'title': _('Warning'),
|
|
'message': _('Your server does not seem to support SSL, you may want to try STARTTLS instead'),
|
|
}
|
|
else:
|
|
self.smtp_port = 25
|
|
return result
|