Odoo 模板发送邮件机制分析及功能扩展

前言

因开源项目“企业微信 For Odoo”需要,需要开发出企业微信用户使用企业微信的“消息推送”功能,改Odoo原使用邮件方式为企业微信消息推送方式

一、分析 “发送邀请邮件”,“发送重置密码邮件”功能 源代码

代码路径:addons\auth_signup\models\res_users.py

def action_reset_password(self):
        """ 为每个用户创建注册令牌,并通过电子邮件发送其注册url """
        if self.env.context.get('install_mode', False):
            return
        if self.filtered(lambda user: not user.active):
            raise UserError(_("您无法对已归档的用户执行此操作."))
        # 准备重置密码注册
        create_mode = bool(self.env.context.get('create_user'))

        # 初次邀请没有时间限制,仅重设密码
        expiration = False if create_mode else now(days=+1)

        self.mapped('partner_id').signup_prepare(signup_type="reset", expiration=expiration)

        # 通过注册网址向用户发送电子邮件
        template = False
        if create_mode:
            try:
                template = self.env.ref('auth_signup.set_password_email', raise_if_not_found=False)
            except ValueError:
                pass
        if not template:
            template = self.env.ref('auth_signup.reset_password_email')
        assert template._name == 'mail.template'

        template_values = {
            'email_to': '${object.email|safe}',
            'email_cc': False,
            'auto_delete': True,
            'partner_to': False,
            'scheduled_date': False,
        }
        template.write(template_values)

        for user in self:
            if not user.email:
                raise UserError(_("无法发送电子邮件:%s用户没有电子邮件地址.", user.name))
            # TDE FIXME:使此模板具有技术性(qweb)
            with self.env.cr.savepoint():
                force_send = not(self.env.context.get('import_file', False))
                template.send_mail(user.id, force_send=force_send, raise_exception=True)
            _logger.info("Password reset email sent for user <%s> to <%s>", user.login, user.email)

上面的代码,我们可以看到 “发送邀请邮件”使用了模板:auth_signup.set_password_email “发送重置密码邮件”使用了模板:auth_signup.reset_password_email 模板文件路径:"\addons\auth_signup\data\auth_signup_data.xml" 使用的模型为:"mail.template" 调用 template.send_mail(user.id, force_send=force_send, raise_exception=True)进行邮件发送

二、分析模型"mail.template"的"send_mail"方法

代码路径:addons\mail\models\mail_template.py

def send_mail(self, res_id, force_send=False, raise_exception=False, email_values=None, notif_layout=False):
        """ 生成一个新的mail.mail。 模板在由res_id和来自模板的模型给定的记录中呈现。
        :param int res_id: 呈现模板的记录的ID
        :param bool force_send: 立即发送电子邮件; 否则使用邮件队列(推荐);
        :param dict email_values: 使用这些值更新生成的邮件,以进一步自定义邮件;
        :param str notif_layout: 可选的通知布局,用于封装生成的电子邮件;
        :returns: 创建的mail.mail的ID
	"""
        # 仅在访问相关文档时才授予对send_mail的访问权限
        self.ensure_one()
        self._send_check_access([res_id])

        Attachment = self.env['ir.attachment']  # TDE FIXME:应从上下文中删除default_type

        # 根据值创建一个mail_mail,不带附件
        values = self.generate_email(res_id, ['subject', 'body_html', 'email_from', 'email_to', 'partner_to', 'email_cc', 'reply_to', 'scheduled_date'])
        values['recipient_ids'] = [(4, pid) for pid in values.get('partner_ids', list())]
        values['attachment_ids'] = [(4, aid) for aid in values.get('attachment_ids', list())]
        values.update(email_values or {})
        attachment_ids = values.pop('attachment_ids', [])
        attachments = values.pop('attachments', [])
        # 添加防止无效的email_from的保护措施
        if 'email_from' in values and not values.get('email_from'):
            values.pop('email_from')
        # 封装体
        if notif_layout and values['body_html']:
            try:
                template = self.env.ref(notif_layout, raise_if_not_found=True)
            except ValueError:
                _logger.warning('QWeb template %s not found when sending template %s. Sending without layouting.' % (notif_layout, self.name))
            else:
                record = self.env[self.model].browse(res_id)
                template_ctx = {
                    'message': self.env['mail.message'].sudo().new(dict(body=values['body_html'], record_name=record.display_name)),
                    'model_description': self.env['ir.model']._get(record._name).display_name,
                    'company': 'company_id' in record and record['company_id'] or self.env.company,
                    'record': record,
                }
                body = template._render(template_ctx, engine='ir.qweb', minimal_qcontext=True)
                values['body_html'] = self.env['mail.render.mixin']._replace_local_links(body)
        mail = self.env['mail.mail'].sudo().create(values)

        # 管理附件
        for attachment in attachments:
            attachment_data = {
                'name': attachment[0],
                'datas': attachment[1],
                'type': 'binary',
                'res_model': 'mail.message',
                'res_id': mail.mail_message_id.id,
            }
            attachment_ids.append((4, Attachment.create(attachment_data).id))
        if attachment_ids:
            mail.write({'attachment_ids': attachment_ids})

        if force_send:
            mail.send(raise_exception=raise_exception)
        return mail.id  # TDE CLEANME:退回邮件+ api.returns?

上面的代码,我们可以看到 模型"mail.template"的方法"send_mail"中,先使用模型"mail.mail"的"create"方法创建邮件,然后使用模型"mail.mail"的"send"方法发送邮件

三、分析模型"mail.mail"的"send"方法

代码路径:addons\mail\models\mail_mail.py

def send(self, auto_commit=False, raise_exception=False):
        """ 
	立即发送选定的电子邮件,而忽略它们的当前状态(除非已被重新发送,否则不应传递已发送的电子邮件)。
	成功发送的电子邮件被标记为“已发送”,而失败发送的电子邮件被标记为“例外”,并且相应的错误邮件将输出到服务器日志中。
	:param bool auto_commit: 发送每封邮件后是否强制提交邮件状态(仅用于调度程序处理);
				在正常交易期间,绝对不能为True(默认值:False)
	:param bool raise_exception: 如果电子邮件发送过程失败,是否引发异常
	:return: True
        """
        for server_id, batch_ids in self._split_by_server():
            smtp_session = None
            try:
                smtp_session = self.env['ir.mail_server'].connect(mail_server_id=server_id)
            except Exception as exc:
                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:
                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()

四、开始写代码

  1. 继承和扩展"\addons\auth_signup\models\res_users.py"中的"action_reset_password"方法:
def action_reset_password(self):
        if self.notification_type == "wxwork":
            # 判断用户的通知类型为企业微信
            # 通过注册网址向用户推送企业微信消息
            # 准备重置密码注册
            create_mode = bool(self.env.context.get("create_user"))

            template = False
            if create_mode:
                try:
                    template = self.env["wxwork.message.template"].search(
                        [("res_id", "=", "auth_signup.set_password_email"),], limit=1,
                    )
                except ValueError:
                    pass
            if not template:
                template = self.env["wxwork.message.template"].search(
                    [("res_id", "=", "auth_signup.reset_password_email"),], limit=1,
                )
        return super(ResUsers, self).action_reset_password()
  1. 继承和扩展"addons\mail\models\mail_template.py"中的方法”send_mail“:
def send_mail(
        self,
        res_id,
        force_send=False,
        raise_exception=False,
        email_values=None,
        notif_layout=False,
    ):
        values = self.generate_email(
            res_id,
            [
                "subject",
                "body_html",
                "email_from",
                "email_to",
                "partner_to",
                "email_cc",
                "reply_to",
                "scheduled_date",
            ],
        )
        # res_id 是用户id
        if self.env["res.users"].browse(res_id).notification_type == "wxwork":
            # 拦截 用户通知类型为企业微信的发送方式
            values = self.env["wxwork.message.template"].generate_message(
                res_id,
                [
                    "subject",
                    "body_html",
                    "email_from",
                    "email_to",
                    "partner_to",
                    "email_cc",
                    "reply_to",
                    "scheduled_date",
                ],
            )

        return super(MailTemplate, self).send_mail(
            res_id,
            force_send=False,
            raise_exception=False,
            email_values=None,
            notif_layout=False,
        )
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章