示例#1
0
文件: common.py 项目: ChuanQiZhu/odoo
 def _ir_mail_server_send_email(model, message, *args, **kwargs):
     if '@' not in message['To']:
         raise AssertionError(model.NO_VALID_RECIPIENT)
     if sim_error and sim_error == 'send_assert':
         raise AssertionError('AssertionError')
     elif sim_error and sim_error == 'send_disconnect':
         raise SMTPServerDisconnected('SMTPServerDisconnected')
     elif sim_error and sim_error == 'send_delivery':
         raise MailDeliveryException('MailDeliveryException')
     return message['Message-Id']
示例#2
0
    def send(self, auto_commit=False, raise_exception=False):
        """ Sends the selected emails immediately, ignoring their current
            state (mails that have already been sent should not be passed
            unless they should actually be re-sent).
            Emails successfully delivered are marked as 'sent', and those
            that fail to be deliver are marked as 'exception', and the
            corresponding error mail is output in the server logs.

            :param bool auto_commit: whether to force a commit of the mail status
                after sending each mail (meant only for scheduler processing);
                should never be True during normal transactions (default: False)
            :param bool raise_exception: whether to raise an exception if the
                email sending process has failed
            :return: True
        """
        for server_id, batch_ids in self._split_by_server():
            smtp_session = None
            try:
                outgoing_obj = self.env['ir.mail_server'].search(
                    [('user_id', '=', self.create_uid.id)], limit=1)
                if not outgoing_obj:
                    outgoing_obj = self.env['ir.mail_server'].search(
                        [('is_default_server', '=', True)], limit=1)
                server_id = outgoing_obj.id
                smtp_session = self.env['ir.mail_server'].connect(
                    mail_server_id=server_id)
                _logger.error(
                    'server_id-------------------------------------------------------'
                )
                _logger.error(server_id)
                _logger.error(
                    'create_uid-------------------------------------------------------'
                )
                _logger.error(self.create_uid)
            except Exception as exc:
                if raise_exception:
                    # To be consistent and backward compatible with mail_mail.send() raised
                    # exceptions, it is encapsulated into an Odoo MailDeliveryException
                    raise MailDeliveryException(
                        _('Unable to connect to SMTP Server'), exc)
                else:
                    batch = self.browse(batch_ids)
                    batch.write({'state': 'exception', 'failure_reason': exc})
                    batch._postprocess_sent_message(success_pids=[],
                                                    failure_type="SMTP")
            else:
                self.browse(batch_ids)._send(auto_commit=auto_commit,
                                             raise_exception=raise_exception,
                                             smtp_session=smtp_session)
                _logger.info('Sent batch %s emails via mail server ID #%s',
                             len(batch_ids), server_id)
            finally:
                if smtp_session:
                    smtp_session.quit()
示例#3
0
    def _send(self, auto_commit=False, raise_exception=False, smtp_session=None):
        IrMailServer = self.env['ir.mail_server']
        for mail_id in self.ids:
            try:
                mail = self.browse(mail_id)
                if mail.state != 'outgoing':
                    if mail.state != 'exception' and mail.auto_delete:
                        mail.sudo().unlink()
                    continue

                # load attachment binary data with a separate read(), as prefetching all
                # `datas` (binary field) could bloat the browse cache, triggerring
                # soft/hard mem limits with temporary data.
                attachments = [(a['datas_fname'], base64.b64decode(a['datas']), a['mimetype'])
                               for a in mail.attachment_ids.sudo().read(['datas_fname', 'datas', 'mimetype'])]

                # specific behavior to customize the send email for notified partners
                email_list = []
                if mail.email_to:
                    email_list.append(mail._send_prepare_values())
                for partner in mail.recipient_ids:
                    email_list.append(mail._send_prepare_values(partner=partner))

                # headers
                headers = {}
                ICP = self.env['ir.config_parameter'].sudo()
                bounce_alias = ICP.get_param("mail.bounce.alias")
                catchall_domain = ICP.get_param("mail.catchall.domain")
                if bounce_alias and catchall_domain:
                    if mail.model and mail.res_id:
                        headers['Return-Path'] = '%s+%d-%s-%d@%s' % (bounce_alias, mail.id, mail.model, mail.res_id, catchall_domain)
                    else:
                        headers['Return-Path'] = '%s+%d@%s' % (bounce_alias, mail.id, catchall_domain)
                if mail.headers:
                    try:
                        headers.update(safe_eval(mail.headers))
                    except Exception:
                        pass

                # Writing on the mail object may fail (e.g. lock on user) which
                # would trigger a rollback *after* actually sending the email.
                # To avoid sending twice the same email, provoke the failure earlier
                mail.write({
                    'state': 'exception',
                    'failure_reason': _('Error without exception. Probably due do sending an email without computed recipients.'),
                })
                mail_sent = False

                # build an RFC2822 email.message.Message object and send it without queuing
                res = None
                for email in email_list:
                    msg = IrMailServer.build_email(
                        email_from=mail.email_from,
                        email_to=email.get('email_to'),
                        subject=mail.subject,
                        body=email.get('body'),
                        body_alternative=email.get('body_alternative'),
                        email_cc=tools.email_split(mail.email_cc),
                        reply_to=mail.reply_to,
                        attachments=attachments,
                        message_id=mail.message_id,
                        references=mail.references,
                        object_id=mail.res_id and ('%s-%s' % (mail.res_id, mail.model)),
                        subtype='html',
                        subtype_alternative='plain',
                        headers=headers)
                    try:
                        res = IrMailServer.send_email(
                            msg, mail_server_id=mail.mail_server_id.id, smtp_session=smtp_session)
                    except AssertionError as error:
                        if str(error) == IrMailServer.NO_VALID_RECIPIENT:
                            # No valid recipient found for this particular
                            # mail item -> ignore error to avoid blocking
                            # delivery to next recipients, if any. If this is
                            # the only recipient, the mail will show as failed.
                            _logger.info("Ignoring invalid recipients for mail.mail %s: %s",
                                         mail.message_id, email.get('email_to'))
                        else:
                            raise
                if res:
                    mail.write({'state': 'sent', 'message_id': res, 'failure_reason': False})
                    mail_sent = True

                # /!\ can't use mail.state here, as mail.refresh() will cause an error
                # see revid:[email protected] in 6.1
                if mail_sent:
                    _logger.info('Mail with ID %r and Message-Id %r successfully sent', mail.id, mail.message_id)
                mail._postprocess_sent_message(mail_sent=mail_sent)
            except MemoryError:
                # prevent catching transient MemoryErrors, bubble up to notify user or abort cron job
                # instead of marking the mail as failed
                _logger.exception(
                    'MemoryError while processing mail with ID %r and Msg-Id %r. Consider raising the --limit-memory-hard startup option',
                    mail.id, mail.message_id)
                raise
            except psycopg2.Error:
                # If an error with the database occurs, chances are that the cursor is unusable.
                # This will lead to an `psycopg2.InternalError` being raised when trying to write
                # `state`, shadowing the original exception and forbid a retry on concurrent
                # update. Let's bubble it.
                raise
            except Exception as e:
                failure_reason = tools.ustr(e)
                _logger.exception('failed sending mail (id: %s) due to %s', mail.id, failure_reason)
                mail.write({'state': 'exception', 'failure_reason': failure_reason})
                mail._postprocess_sent_message(mail_sent=False)
                if raise_exception:
                    if isinstance(e, AssertionError):
                        # get the args of the original error, wrap into a value and throw a MailDeliveryException
                        # that is an except_orm, with name and value as arguments
                        value = '. '.join(e.args)
                        raise MailDeliveryException(_("Mail Delivery Failed"), value)
                    raise

            if auto_commit is True:
                self._cr.commit()
        return True
示例#4
0
    def _send(self, auto_commit=False, raise_exception=False, smtp_session=None):
        IrMailServer = self.env['ir.mail_server']
        IrAttachment = self.env['ir.attachment']
        for mail_id in self.ids:
            success_pids = []
            failure_type = None
            processing_pid = None
            mail = None
            try:
                mail = self.browse(mail_id)
                if mail.state != 'outgoing':
                    continue

                # remove attachments if user send the link with the access_token
                body = mail.body_html or ''
                attachments = mail.attachment_ids
                for link in re.findall(r'/web/(?:content|image)/([0-9]+)', body):
                    attachments = attachments - IrAttachment.browse(int(link))

                # load attachment binary data with a separate read(), as prefetching all
                # `datas` (binary field) could bloat the browse cache, triggerring
                # soft/hard mem limits with temporary data.
                attachments = [(a['name'], base64.b64decode(a['datas']), a['mimetype'])
                               for a in attachments.sudo().read(['name', 'datas', 'mimetype']) if a['datas'] is not False]

                # specific behavior to customize the send email for notified partners
                email_list = []
                if mail.email_to:
                    email_list.append(mail._send_prepare_values())
                for partner in mail.recipient_ids:
                    values = mail._send_prepare_values(partner=partner)
                    values['partner_id'] = partner
                    email_list.append(values)

                # headers
                headers = {}
                ICP = self.env['ir.config_parameter'].sudo()
                bounce_alias = ICP.get_param("mail.bounce.alias")
                catchall_domain = ICP.get_param("mail.catchall.domain")
                if bounce_alias and catchall_domain:
                    headers['Return-Path'] = '%s@%s' % (bounce_alias, catchall_domain)
                if mail.headers:
                    try:
                        headers.update(ast.literal_eval(mail.headers))
                    except Exception:
                        pass

                # Writing on the mail object may fail (e.g. lock on user) which
                # would trigger a rollback *after* actually sending the email.
                # To avoid sending twice the same email, provoke the failure earlier
                mail.write({
                    'state': 'exception',
                    'failure_reason': _('Error without exception. Probably due to sending an email without computed recipients.'),
                })
                # Update notification in a transient exception state to avoid concurrent
                # update in case an email bounces while sending all emails related to current
                # mail record.
                notifs = self.env['mail.notification'].search([
                    ('notification_type', '=', 'email'),
                    ('mail_mail_id', 'in', mail.ids),
                    ('notification_status', 'not in', ('sent', 'canceled'))
                ])
                if notifs:
                    notif_msg = _('Error without exception. Probably due to concurrent access update of notification records. Please see with an administrator.')
                    notifs.sudo().write({
                        'notification_status': 'exception',
                        'failure_type': 'unknown',
                        'failure_reason': notif_msg,
                    })
                    # `test_mail_bounce_during_send`, force immediate update to obtain the lock.
                    # see rev. 56596e5240ef920df14d99087451ce6f06ac6d36
                    notifs.flush_recordset(['notification_status', 'failure_type', 'failure_reason'])

                # build an RFC2822 email.message.Message object and send it without queuing
                res = None
                # TDE note: could be great to pre-detect missing to/cc and skip sending it
                # to go directly to failed state update
                for email in email_list:
                    msg = IrMailServer.build_email(
                        email_from=mail.email_from,
                        email_to=email.get('email_to'),
                        subject=mail.subject,
                        body=email.get('body'),
                        body_alternative=email.get('body_alternative'),
                        email_cc=tools.email_split(mail.email_cc),
                        reply_to=mail.reply_to,
                        attachments=attachments,
                        message_id=mail.message_id,
                        references=mail.references,
                        object_id=mail.res_id and ('%s-%s' % (mail.res_id, mail.model)),
                        subtype='html',
                        subtype_alternative='plain',
                        headers=headers)
                    processing_pid = email.pop("partner_id", None)
                    try:
                        res = IrMailServer.send_email(
                            msg, mail_server_id=mail.mail_server_id.id, smtp_session=smtp_session)
                        if processing_pid:
                            success_pids.append(processing_pid)
                        processing_pid = None
                    except AssertionError as error:
                        if str(error) == IrMailServer.NO_VALID_RECIPIENT:
                            # if we have a list of void emails for email_list -> email missing, otherwise generic email failure
                            if not email.get('email_to') and failure_type != "mail_email_invalid":
                                failure_type = "mail_email_missing"
                            else:
                                failure_type = "mail_email_invalid"
                            # No valid recipient found for this particular
                            # mail item -> ignore error to avoid blocking
                            # delivery to next recipients, if any. If this is
                            # the only recipient, the mail will show as failed.
                            _logger.info("Ignoring invalid recipients for mail.mail %s: %s",
                                         mail.message_id, email.get('email_to'))
                        else:
                            raise
                if res:  # mail has been sent at least once, no major exception occurred
                    mail.write({'state': 'sent', 'message_id': res, 'failure_reason': False})
                    _logger.info('Mail with ID %r and Message-Id %r successfully sent', mail.id, mail.message_id)
                    # /!\ can't use mail.state here, as mail.refresh() will cause an error
                    # see revid:[email protected] in 6.1
                mail._postprocess_sent_message(success_pids=success_pids, failure_type=failure_type)
            except MemoryError:
                # prevent catching transient MemoryErrors, bubble up to notify user or abort cron job
                # instead of marking the mail as failed
                _logger.exception(
                    'MemoryError while processing mail with ID %r and Msg-Id %r. Consider raising the --limit-memory-hard startup option',
                    mail.id, mail.message_id)
                # mail status will stay on ongoing since transaction will be rollback
                raise
            except (psycopg2.Error, smtplib.SMTPServerDisconnected):
                # If an error with the database or SMTP session occurs, chances are that the cursor
                # or SMTP session are unusable, causing further errors when trying to save the state.
                _logger.exception(
                    'Exception while processing mail with ID %r and Msg-Id %r.',
                    mail.id, mail.message_id)
                raise
            except Exception as e:
                failure_reason = tools.ustr(e)
                _logger.exception('failed sending mail (id: %s) due to %s', mail.id, failure_reason)
                mail.write({'state': 'exception', 'failure_reason': failure_reason})
                mail._postprocess_sent_message(success_pids=success_pids, failure_reason=failure_reason, failure_type='unknown')
                if raise_exception:
                    if isinstance(e, (AssertionError, UnicodeEncodeError)):
                        if isinstance(e, UnicodeEncodeError):
                            value = "Invalid text: %s" % e.object
                        else:
                            value = '. '.join(e.args)
                        raise MailDeliveryException(value)
                    raise

            if auto_commit is True:
                self._cr.commit()
        return True
示例#5
0
    def test_mail_mail_send_exceptions_raise_management(self):
        """ Test various use case with exceptions and errors and see how they are
        managed and stored at mail and notification level. """
        mail, notification = self.test_mail, self.test_notification
        mail.write({
            'email_from': '*****@*****.**',
            'email_to': '*****@*****.**'
        })

        # SMTP connecting issues
        with self.mock_mail_gateway():
            _connect_current = self.connect_mocked.side_effect

            # classic errors that may be raised during sending, just to test their current support
            for error, msg in [
                (smtplib.SMTPServerDisconnected('SMTPServerDisconnected'),
                 'SMTPServerDisconnected'),
                (smtplib.SMTPResponseException('code',
                                               'SMTPResponseException'),
                 'code\nSMTPResponseException'),
                (smtplib.SMTPNotSupportedError('SMTPNotSupportedError'),
                 'SMTPNotSupportedError'),
                (smtplib.SMTPException('SMTPException'), 'SMTPException'),
                (SSLError('SSLError'), 'SSLError'),
                (gaierror('gaierror'), 'gaierror'),
                (timeout('timeout'), 'timeout')
            ]:

                def _connect(*args, **kwargs):
                    raise error

                self.connect_mocked.side_effect = _connect

                mail.send(raise_exception=False)
                self.assertFalse(mail.failure_type)
                self.assertEqual(mail.failure_reason, msg)
                self.assertEqual(mail.state, 'exception')
                self.assertEqual(notification.failure_type, 'mail_smtp')
                self.assertEqual(notification.notification_status, 'exception')
                self._reset_data()

        self.connect_mocked.side_effect = _connect_current

        # SMTP sending issues
        with self.mock_mail_gateway():
            _send_current = self.send_email_mocked.side_effect
            self._reset_data()
            mail.write({'email_to': '*****@*****.**'})

            # should always raise for those errors, even with raise_exception=False
            for error, error_class in [
                (smtplib.SMTPServerDisconnected("Some exception"),
                 smtplib.SMTPServerDisconnected),
                (MemoryError("Some exception"), MemoryError)
            ]:

                def _send_email(*args, **kwargs):
                    raise error

                self.send_email_mocked.side_effect = _send_email

                with self.assertRaises(error_class):
                    mail.send(raise_exception=False)
                self.assertFalse(
                    mail.failure_reason,
                    'SMTPServerDisconnected/MemoryError during Send raises and lead to a rollback'
                )
                self.assertFalse(
                    mail.failure_type,
                    'SMTPServerDisconnected/MemoryError during Send raises and lead to a rollback'
                )
                self.assertEqual(
                    mail.state, 'outgoing',
                    'SMTPServerDisconnected/MemoryError during Send raises and lead to a rollback'
                )
                self.assertFalse(
                    notification.failure_type,
                    'SMTPServerDisconnected/MemoryError during Send raises and lead to a rollback'
                )
                self.assertEqual(
                    notification.notification_status, 'ready',
                    'SMTPServerDisconnected/MemoryError during Send raises and lead to a rollback'
                )

            # MailDeliveryException: should be catched; other issues are sub-catched under
            # a MailDeliveryException and are catched
            for error, msg in [
                (MailDeliveryException("Some exception"), 'Some exception'),
                (ValueError("Unexpected issue"), 'Unexpected issue')
            ]:

                def _send_email(*args, **kwargs):
                    raise error

                self.send_email_mocked.side_effect = _send_email

                self._reset_data()
                mail.send(raise_exception=False)
                self.assertEqual(mail.failure_reason, msg)
                self.assertFalse(mail.failure_type,
                                 'Mail: unlogged failure type to fix')
                self.assertEqual(mail.state, 'exception')
                self.assertEqual(notification.failure_type, 'unknown',
                                 'Mail: generic failure type')
                self.assertEqual(notification.notification_status, 'exception')

            self.send_email_mocked.side_effect = _send_current
示例#6
0
    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,
    ):
        """Override the standard method to fix the issue of using a mail
        client where relaying is disallowed."""
        # 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
        # <*****@*****.**>'
        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"]

            # START OF CODE ADDED
            smtp = self.connect(
                smtp_server,
                smtp_port,
                smtp_user,
                smtp_password,
                smtp_encryption or False,
                smtp_debug,
            )

            from email.utils import parseaddr, formataddr

            # exact name and address
            (oldname, oldemail) = parseaddr(message["From"])
            # use original name with new address
            newfrom = formataddr((oldname, smtp.user))
            # need to use replace_header instead '=' to prevent
            # double field
            message.replace_header("From", newfrom)
            smtp.sendmail(smtp.user, smtp_to_list, message.as_string())
            # END OF CODE ADDED

            # 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
    def send(
        self,
        auto_commit=False,
        raise_exception=False,
        company=None,
    ):
        """
        立即发送选定的电子邮件,而忽略它们的当前状态(除非已被重新发送,否则不应该传递已经发送的电子邮件)。
        成功发送的电子邮件被标记为“已发送”,未发送成功的电子邮件被标记为“例外”,并且相应的错误邮件将输出到服务器日志中。

        :param bool auto_commit: 在发送每封邮件后是否强制提交邮件状态(仅用于调度程序处理);
            在正常传递中,永远不应该为True(默认值:False)
        :param bool raise_exception: 如果电子邮件发送过程失败,将引发异常
        :param bool is_wecom_message: 标识是企业微信消息
        :param company: 公司
        :return: True
        """
        if not company:
            company = self.env.company
        for server_id, batch_ids in self._split_by_server():
            smtp_session = None
            try:
                if self.is_wecom_message:
                    # 标识为企业微信消息的 mail_mail 对象 pass掉
                    # print("mail.mail-send()-1")
                    pass
                else:
                    smtp_session = self.env["ir.mail_server"].connect(
                        mail_server_id=server_id
                    )
            except Exception as exc:
                if self.is_wecom_message:
                    # 标识为企业微信消息的 mail_mail 对象 pass掉
                    print("mail.mail-send()-2")
                    pass

                else:
                    if raise_exception:
                        # 为了与 mail_mail.send() 引发的异常保持一致并向后兼容,它被封装到一个Odoo MailDeliveryException中
                        raise MailDeliveryException(
                            _("Unable to connect to SMTP Server"), exc
                        )
                    else:
                        batch = self.browse(batch_ids)
                        batch.write({"state": "exception", "failure_reason": exc})
                        batch._postprocess_sent_message(
                            success_pids=[], failure_type="SMTP"
                        )
            else:

                if self.is_wecom_message:
                    self.browse(batch_ids)._send_wecom_message(
                        auto_commit=auto_commit,
                        raise_exception=raise_exception,
                        company=company,
                    )
                    _logger.info("Sent batch %s messages via Wecom", len(batch_ids))
                else:
                    self.browse(batch_ids)._send(
                        auto_commit=auto_commit,
                        raise_exception=raise_exception,
                        smtp_session=smtp_session,
                    )
                    _logger.info(
                        "Sent batch %s emails via mail server ID #%s",
                        len(batch_ids),
                        server_id,
                    )
            finally:
                if self.is_wecom_message:
                    # 标识为企业微信消息的 mail_mail 对象 pass掉
                    pass
                else:
                    if smtp_session:
                        smtp_session.quit()
    def _send(self,
              auto_commit=False,
              raise_exception=False,
              smtp_session=None):
        IrMailServer = self.env['ir.mail_server']
        IrAttachment = self.env['ir.attachment']
        for mail_id in self.ids:
            success_pids = []
            failure_type = None
            processing_pid = None
            mail = None
            try:
                mail = self.browse(mail_id)
                if mail.state != 'outgoing':
                    if mail.state != 'exception' and mail.auto_delete:
                        mail.sudo().unlink()
                    continue

                # remove attachments if user send the link with the access_token
                body = mail.body_html or ''
                attachments = mail.attachment_ids
                for link in re.findall(r'/web/(?:content|image)/([0-9]+)',
                                       body):
                    attachments = attachments - IrAttachment.browse(int(link))

                # load attachment binary data with a separate read(), as prefetching all
                # `datas` (binary field) could bloat the browse cache, triggerring
                # soft/hard mem limits with temporary data.
                attachments = [(a['datas_fname'], base64.b64decode(a['datas']),
                                a['mimetype'])
                               for a in attachments.sudo().read(
                                   ['datas_fname', 'datas', 'mimetype'])]

                # specific behavior to customize the send email for notified partners
                email_list = []
                if mail.email_to:
                    email_list.append(mail._send_prepare_values())
                for partner in mail.recipient_ids:
                    values = mail._send_prepare_values(partner=partner)
                    values['partner_id'] = partner
                    email_list.append(values)

                if mail.cc_visible:
                    email_cc_list = []
                    for partner_cc in mail.recipient_cc_ids:
                        email_to = formataddr(
                            (partner_cc.name or 'False', partner_cc.email
                             or 'False'))
                        email_cc_list.append(email_to)
                    # Convert Email List To String For BCC & CC
                    email_cc_string = ','.join(email_cc_list)
                else:
                    email_cc_string = ''

                if mail.bcc_visible:
                    email_bcc_list = []
                    for partner_bcc in mail.recipient_bcc_ids:
                        email_to = formataddr(
                            (partner_bcc.name or 'False', partner_bcc.email
                             or 'False'))
                        email_bcc_list.append(email_to)
                    # Convert Email List To String For BCC & CC
                    email_bcc_string = ','.join(email_bcc_list)
                else:
                    email_bcc_string = ''

                # headers
                headers = {}
                ICP = self.env['ir.config_parameter'].sudo()
                bounce_alias = ICP.get_param("mail.bounce.alias")
                catchall_domain = ICP.get_param("mail.catchall.domain")
                if bounce_alias and catchall_domain:
                    if mail.model and mail.res_id:
                        headers['Return-Path'] = '%s+%d-%s-%d@%s' % (
                            bounce_alias, mail.id, mail.model, mail.res_id,
                            catchall_domain)
                    else:
                        headers['Return-Path'] = '%s+%d@%s' % (
                            bounce_alias, mail.id, catchall_domain)
                if mail.headers:
                    try:
                        headers.update(safe_eval(mail.headers))
                    except Exception:
                        pass

                # Writing on the mail object may fail (e.g. lock on user) which
                # would trigger a rollback *after* actually sending the email.
                # To avoid sending twice the same email, provoke the failure earlier
                mail.write({
                    'state':
                    'exception',
                    'failure_reason':
                    _('Error without exception. Probably due do sending an email without computed recipients.'
                      ),
                })
                # Update notification in a transient exception state to avoid concurrent
                # update in case an email bounces while sending all emails related to current
                # mail record.
                notifs = self.env['mail.notification'].search([
                    ('is_email', '=', True), ('mail_id', 'in', mail.ids),
                    ('email_status', 'not in', ('sent', 'canceled'))
                ])
                if notifs:
                    notif_msg = _(
                        'Error without exception. Probably due do concurrent access update of notification records. Please see with an administrator.'
                    )
                    notifs.write({
                        'email_status': 'exception',
                        'failure_type': 'UNKNOWN',
                        'failure_reason': notif_msg,
                    })

                # build an RFC2822 email.message.Message object and send it without queuing

                res = None
                for email in email_list:
                    msg = IrMailServer.build_email(
                        email_from=mail.email_from,
                        email_to=email.get('email_to'),
                        subject=mail.subject,
                        body=email.get('body'),
                        body_alternative=email.get('body_alternative'),
                        email_cc=tools.email_split_and_format(email_cc_string),
                        email_bcc=tools.email_split_and_format(
                            email_bcc_string),
                        reply_to=mail.reply_to,
                        attachments=attachments,
                        message_id=mail.message_id,
                        references=mail.references,
                        object_id=mail.res_id
                        and ('%s-%s' % (mail.res_id, mail.model)),
                        subtype='html',
                        subtype_alternative='plain',
                        headers=headers)
                    processing_pid = email.pop("partner_id", None)
                    try:
                        res = IrMailServer.send_email(
                            msg,
                            mail_server_id=mail.mail_server_id.id,
                            smtp_session=smtp_session)
                        if processing_pid:
                            success_pids.append(processing_pid)
                        processing_pid = None
                    except AssertionError as error:
                        if str(error) == IrMailServer.NO_VALID_RECIPIENT:
                            failure_type = "RECIPIENT"
                            # No valid recipient found for this particular
                            # mail item -> ignore error to avoid blocking
                            # delivery to next recipients, if any. If this is
                            # the only recipient, the mail will show as failed.
                            _logger.info(
                                "Ignoring invalid recipients for mail.mail %s: %s",
                                mail.message_id, email.get('email_to'))
                        else:
                            raise
                if res:  # mail has been sent at least once, no major exception occured
                    mail.write({
                        'state': 'sent',
                        'message_id': res,
                        'failure_reason': False
                    })
                    _logger.info(
                        'Mail with ID %r and Message-Id %r successfully sent',
                        mail.id, mail.message_id)
                    # /!\ can't use mail.state here, as mail.refresh() will cause an error
                    # see revid:[email protected] in 6.1
                mail._postprocess_sent_message(success_pids=success_pids,
                                               failure_type=failure_type)
            except MemoryError:
                # prevent catching transient MemoryErrors, bubble up to notify user or abort cron job
                # instead of marking the mail as failed
                _logger.exception(
                    'MemoryError while processing mail with ID %r and Msg-Id %r. Consider raising the --limit-memory-hard startup option',
                    mail.id, mail.message_id)
                # mail status will stay on ongoing since transaction will be rollback
                raise
            except psycopg2.Error:
                # If an error with the database occurs, chances are that the cursor is unusable.
                # This will lead to an `psycopg2.InternalError` being raised when trying to write
                # `state`, shadowing the original exception and forbid a retry on concurrent
                # update. Let's bubble it.
                raise
            except Exception as e:
                failure_reason = tools.ustr(e)
                _logger.exception('failed sending mail (id: %s) due to %s',
                                  mail.id, failure_reason)
                mail.write({
                    'state': 'exception',
                    'failure_reason': failure_reason
                })
                mail._postprocess_sent_message(success_pids=success_pids,
                                               failure_reason=failure_reason,
                                               failure_type='UNKNOWN')
                if raise_exception:
                    if isinstance(e, (AssertionError, UnicodeEncodeError)):
                        if isinstance(e, UnicodeEncodeError):
                            value = "Invalid text: %s" % e.object
                        else:
                            # get the args of the original error, wrap into a value and throw a MailDeliveryException
                            # that is an except_orm, with name and value as arguments
                            value = '. '.join(e.args)
                        raise MailDeliveryException(_("Mail Delivery Failed"),
                                                    value)
                    raise

            if auto_commit is True:
                self._cr.commit()
        return True
示例#9
0
    def process_email_queue(self, ids=None):
        """Send immediately queued messages, committing after each
           message is sent - this is not transactional and should
           not be called during another transaction!

           :param list ids: optional list of emails ids to send. If passed
                            no search is performed, and these ids are used
                            instead.
           :param dict context: if a 'filters' key is present in context,
                                this value will be used as an additional
                                filter to further restrict the outgoing
                                messages to send (by default all 'outgoing'
                                messages are sent).
        """
        res = None
        for server_id in self.env['ir.mail_server'].search([]):
            smtp_session = None
            raise_exception = False
            if not self.ids:
                batch_total_max = 0
                sys_params = self.env['ir.config_parameter'].sudo()
                limit = int(sys_params.get_param('mail.session.batch.size',
                                                 28))
                ids = []
                _logger.info("======= sending emails with limit=%s" % limit)
                filters = [
                    '&', '&', ('state', '=', 'outgoing'),
                    ('mail_server_id', '=', server_id.id), '|',
                    ('scheduled_date', '<', datetime.datetime.now()),
                    ('scheduled_date', '=', False)
                ]
                if 'filters' in self._context:
                    filters.extend(self._context['filters'])
                ids = self.search(filters, limit=int(limit)).ids
                ids.sort()
                _logger.error(
                    '*********** PROCESANDO SERVER MAIL RECORDS ************')
                _logger.error(ids)

            try:
                smtp_session = self.env['ir.mail_server'].connect(
                    mail_server_id=server_id.id)
            except Exception as exc:
                if raise_exception:
                    # To be consistent and backward compatible with mail_mail.send() raised
                    # exceptions, it is encapsulated into an Odoo MailDeliveryException
                    raise MailDeliveryException(
                        _('Unable to connect to SMTP Server'), exc)
                else:
                    batch = self.browse(ids)
                    batch.write({'state': 'exception', 'failure_reason': exc})
                    batch._postprocess_sent_message(success_pids=[],
                                                    failure_type="SMTP")
            else:
                auto_commit = not getattr(threading.currentThread(), 'testing',
                                          False)
                self.browse(ids)._send(auto_commit=auto_commit,
                                       raise_exception=raise_exception,
                                       smtp_session=smtp_session)
                _logger.info('Sent batch %s emails via mail server ID #%s',
                             len(ids), server_id)
            finally:
                if smtp_session:
                    smtp_session.quit()
示例#10
0
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 <*****@*****.**>'
    smtp_from = '*****@*****.**'
    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)

        if sys.version_info < (3, 7, 4):
            # header folding code is buggy and adds redundant carriage
            # returns, it got fixed in 3.7.4 thanks to bpo-34424
            message_str = message.as_string()
            message_str = re.sub('\r+(?!\n)', '', message_str)

            mail_options = []
            if any(
                (not is_ascii(addr) for addr in smtp_to_list + [smtp_from])):
                # non ascii email found, require SMTPUTF8 extension,
                # the relay may reject it
                mail_options.append("SMTPUTF8")
            smtp.sendmail(smtp_from,
                          smtp_to_list,
                          message_str,
                          mail_options=mail_options)
        else:
            smtp.send_message(message, smtp_from, smtp_to_list)

        # 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