Tom Palan
8 years ago
9 changed files with 498 additions and 0 deletions
-
1README.md
-
53mail_bcc/README.rst
-
7mail_bcc/__init__.py
-
44mail_bcc/__openerp__.py
-
8mail_bcc/models/__init__.py
-
160mail_bcc/models/mail_mail.py
-
148mail_bcc/models/mail_template.py
-
62mail_bcc/static/description/index.html
-
15mail_bcc/views/views.xml
@ -0,0 +1,53 @@ |
|||||
|
.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg |
||||
|
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html |
||||
|
:alt: License: AGPL-3 |
||||
|
|
||||
|
============== |
||||
|
mail_bcc |
||||
|
============== |
||||
|
|
||||
|
This module extends the functionality of mail to support BCC addresses in mail templates. |
||||
|
|
||||
|
|
||||
|
Usage |
||||
|
===== |
||||
|
|
||||
|
To use this module, you need to edit your mail template and enter an email address in the BCC field. Every time you use this mail template, a copy of the message is sent to this address as blind copy. |
||||
|
|
||||
|
|
||||
|
Bug Tracker |
||||
|
=========== |
||||
|
|
||||
|
Bugs are tracked on `GitHub Issues |
||||
|
<https://github.com/OCA/social/issues>`_. In case of trouble, please |
||||
|
check there if your issue has already been reported. If you spotted it first, |
||||
|
help us smashing it by providing a detailed and welcomed feedback. |
||||
|
|
||||
|
Credits |
||||
|
======= |
||||
|
|
||||
|
Images |
||||
|
------ |
||||
|
|
||||
|
* Odoo Community Association: `Icon <https://github.com/OCA/maintainer-tools/blob/master/template/module/static/description/icon.svg>`_. |
||||
|
|
||||
|
Contributors |
||||
|
------------ |
||||
|
|
||||
|
* Juan Formoso <jfv@anubia.es> |
||||
|
* Tom Palan <thomas@palan.at> |
||||
|
|
||||
|
Maintainer |
||||
|
---------- |
||||
|
|
||||
|
.. image:: https://odoo-community.org/logo.png |
||||
|
:alt: Odoo Community Association |
||||
|
:target: https://odoo-community.org |
||||
|
|
||||
|
This module is maintained by the OCA. |
||||
|
|
||||
|
OCA, or the Odoo Community Association, is a nonprofit organization whose |
||||
|
mission is to support the collaborative development of Odoo features and |
||||
|
promote its widespread use. |
||||
|
|
||||
|
To contribute to this module, please visit https://odoo-community.org. |
@ -0,0 +1,7 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
################################################################ |
||||
|
# License, author and contributors information in: # |
||||
|
# __openerp__.py file at the root folder of this module. # |
||||
|
################################################################ |
||||
|
|
||||
|
from . import models |
@ -0,0 +1,44 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
############################################################################## |
||||
|
# |
||||
|
# mail_bcc module for odoo v9 |
||||
|
# Copyright (C) 2015 Anubía, soluciones en la nube,SL (http://www.anubia.es) |
||||
|
# @author: Juan Formoso <jfv@anubia.es>, |
||||
|
# @author: Tom Palan <thomas@palan.at>, |
||||
|
# |
||||
|
# This program is free software: you can redistribute it and/or modify |
||||
|
# it under the terms of the GNU Affero General Public License as |
||||
|
# published by the Free Software Foundation, either version 3 of the |
||||
|
# License, or (at your option) any later version. |
||||
|
# |
||||
|
# This program is distributed in the hope that it will be useful, |
||||
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
||||
|
# GNU Affero General Public License for more details. |
||||
|
# |
||||
|
# You should have received a copy of the GNU Affero General Public License |
||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>. |
||||
|
# |
||||
|
############################################################################## |
||||
|
|
||||
|
{ |
||||
|
'name': 'Mail BCC', |
||||
|
'summary': 'Blind Carbon Copy available on mails', |
||||
|
'description': """ |
||||
|
Adds a BCC field to mail templates and uses them to send a separate copy of the mail to the BCC recipient. |
||||
|
""", |
||||
|
'version': '0.1', |
||||
|
'license': 'AGPL-3', |
||||
|
'author': 'Juan Formoso <jfv@anubia.es>, Tom Palan <thomas@palan.at>', |
||||
|
'website': 'http://www.anubia.es', |
||||
|
'category': 'Mail', |
||||
|
'depends': [ |
||||
|
'mail', |
||||
|
], |
||||
|
'data': [ |
||||
|
"views/views.xml" |
||||
|
], |
||||
|
'demo': [], |
||||
|
'test': [], |
||||
|
'installable': True, |
||||
|
} |
@ -0,0 +1,8 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
################################################################ |
||||
|
# License, author and contributors information in: # |
||||
|
# __openerp__.py file at the root folder of this module. # |
||||
|
################################################################ |
||||
|
|
||||
|
from . import mail_mail |
||||
|
from . import mail_template |
@ -0,0 +1,160 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
################################################################ |
||||
|
# License, author and contributors information in: # |
||||
|
# __openerp__.py file at the root folder of this module. # |
||||
|
################################################################ |
||||
|
|
||||
|
import base64 |
||||
|
import logging |
||||
|
from email.utils import formataddr |
||||
|
|
||||
|
import psycopg2 |
||||
|
|
||||
|
from openerp import _, api, fields, models |
||||
|
from openerp import tools |
||||
|
from openerp.addons.base.ir.ir_mail_server import MailDeliveryException |
||||
|
from openerp.tools.safe_eval import safe_eval as eval |
||||
|
|
||||
|
_logger = logging.getLogger(__name__) |
||||
|
|
||||
|
|
||||
|
class MailMail(models.Model): |
||||
|
_inherit = 'mail.mail' |
||||
|
|
||||
|
email_bcc = fields.Char(string='BCC', |
||||
|
help='Blind carbon copy message recipients') |
||||
|
|
||||
|
@api.multi |
||||
|
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 |
||||
|
""" |
||||
|
IrMailServer = self.env['ir.mail_server'] |
||||
|
|
||||
|
for mail in self: |
||||
|
try: |
||||
|
# TDE note: remove me when model_id field is present on mail.message - done here to avoid doing it multiple times in the sub method |
||||
|
if mail.model: |
||||
|
model = self.env['ir.model'].sudo().search([('model', '=', mail.model)])[0] |
||||
|
else: |
||||
|
model = None |
||||
|
if model: |
||||
|
mail = mail.with_context(model_name=model.name) |
||||
|
|
||||
|
# 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'])) |
||||
|
for a in mail.attachment_ids.sudo().read(['datas_fname', 'datas'])] |
||||
|
|
||||
|
# specific behavior to customize the send email for notified partners |
||||
|
email_list = [] |
||||
|
if mail.email_to: |
||||
|
email_list.append(mail.send_get_email_dict()) |
||||
|
for partner in mail.recipient_ids: |
||||
|
email_list.append(mail.send_get_email_dict(partner=partner)) |
||||
|
|
||||
|
# headers |
||||
|
headers = {} |
||||
|
bounce_alias = self.env['ir.config_parameter'].get_param("mail.bounce.alias") |
||||
|
catchall_domain = self.env['ir.config_parameter'].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(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), |
||||
|
email_bcc=tools.email_split(mail.email_bcc), |
||||
|
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) |
||||
|
except AssertionError as error: |
||||
|
if error.message == 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:odo@openerp.com-20120622152536-42b2s28lvdv3odyr 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_v9(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_v9(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 |
@ -0,0 +1,148 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
################################################################ |
||||
|
# License, author and contributors information in: # |
||||
|
# __openerp__.py file at the root folder of this module. # |
||||
|
################################################################ |
||||
|
|
||||
|
import base64 |
||||
|
import copy |
||||
|
import datetime |
||||
|
import dateutil.relativedelta as relativedelta |
||||
|
import logging |
||||
|
import lxml |
||||
|
import urlparse |
||||
|
import openerp |
||||
|
from urllib import urlencode, quote as quote |
||||
|
|
||||
|
from openerp import _, api, fields, models, SUPERUSER_ID |
||||
|
from openerp import tools |
||||
|
from openerp import report as odoo_report |
||||
|
from openerp.exceptions import UserError |
||||
|
|
||||
|
_logger = logging.getLogger(__name__) |
||||
|
|
||||
|
|
||||
|
class MailTemplate(models.Model): |
||||
|
_inherit = 'mail.template' |
||||
|
|
||||
|
email_bcc = fields.Char(string='Bcc', |
||||
|
help='Blind carbon copy message recipients') |
||||
|
|
||||
|
@api.multi |
||||
|
def generate_email(self, res_ids, fields=None): |
||||
|
"""Generates an email from the template for given the given model based on |
||||
|
records given by res_ids. |
||||
|
|
||||
|
:param template_id: id of the template to render. |
||||
|
:param res_id: id of the record to use for rendering the template (model |
||||
|
is taken from template definition) |
||||
|
:returns: a dict containing all relevant fields for creating a new |
||||
|
mail.mail entry, with one extra key ``attachments``, in the |
||||
|
format [(report_name, data)] where data is base64 encoded. |
||||
|
""" |
||||
|
self.ensure_one() |
||||
|
multi_mode = True |
||||
|
if isinstance(res_ids, (int, long)): |
||||
|
res_ids = [res_ids] |
||||
|
multi_mode = False |
||||
|
if fields is None: |
||||
|
fields = ['subject', 'body_html', 'email_from', 'email_to', 'partner_to', 'email_cc', 'reply_to'] |
||||
|
fields = fields + ['email_bcc'] |
||||
|
|
||||
|
res_ids_to_templates = self.get_email_template_batch(res_ids) |
||||
|
|
||||
|
# templates: res_id -> template; template -> res_ids |
||||
|
templates_to_res_ids = {} |
||||
|
for res_id, template in res_ids_to_templates.iteritems(): |
||||
|
templates_to_res_ids.setdefault(template, []).append(res_id) |
||||
|
|
||||
|
results = dict() |
||||
|
for template, template_res_ids in templates_to_res_ids.iteritems(): |
||||
|
Template = self.env['mail.template'] |
||||
|
# generate fields value for all res_ids linked to the current template |
||||
|
if template.lang: |
||||
|
Template = Template.with_context(lang=template._context.get('lang')) |
||||
|
for field in fields: |
||||
|
Template = Template.with_context(safe=field in {'subject'}) |
||||
|
generated_field_values = Template.render_template( |
||||
|
getattr(template, field), template.model, template_res_ids, |
||||
|
post_process=(field == 'body_html')) |
||||
|
for res_id, field_value in generated_field_values.iteritems(): |
||||
|
results.setdefault(res_id, dict())[field] = field_value |
||||
|
# compute recipients |
||||
|
if any(field in fields for field in ['email_to', 'partner_to', 'email_cc']): |
||||
|
results = template.generate_recipients(results, template_res_ids) |
||||
|
# update values for all res_ids |
||||
|
for res_id in template_res_ids: |
||||
|
values = results[res_id] |
||||
|
# body: add user signature, sanitize |
||||
|
if 'body_html' in fields and template.user_signature: |
||||
|
signature = self.env.user.signature |
||||
|
if signature: |
||||
|
values['body_html'] = tools.append_content_to_html(values['body_html'], signature, plaintext=False) |
||||
|
if values.get('body_html'): |
||||
|
values['body'] = tools.html_sanitize(values['body_html']) |
||||
|
# technical settings |
||||
|
values.update( |
||||
|
mail_server_id=template.mail_server_id.id or False, |
||||
|
auto_delete=template.auto_delete, |
||||
|
model=template.model, |
||||
|
res_id=res_id or False, |
||||
|
attachment_ids=[attach.id for attach in template.attachment_ids], |
||||
|
) |
||||
|
|
||||
|
# Add report in attachments: generate once for all template_res_ids |
||||
|
if template.report_template and not 'report_template_in_attachment' in self.env.context: |
||||
|
for res_id in template_res_ids: |
||||
|
attachments = [] |
||||
|
report_name = self.render_template(template.report_name, template.model, res_id) |
||||
|
report = template.report_template |
||||
|
report_service = report.report_name |
||||
|
|
||||
|
if report.report_type in ['qweb-html', 'qweb-pdf']: |
||||
|
result, format = self.pool['report'].get_pdf(self._cr, self._uid, [res_id], report_service, context=Template._context), 'pdf' |
||||
|
else: |
||||
|
result, format = odoo_report.render_report(self._cr, self._uid, [res_id], report_service, {'model': template.model}, Template._context) |
||||
|
|
||||
|
# TODO in trunk, change return format to binary to match message_post expected format |
||||
|
result = base64.b64encode(result) |
||||
|
if not report_name: |
||||
|
report_name = 'report.' + report_service |
||||
|
ext = "." + format |
||||
|
if not report_name.endswith(ext): |
||||
|
report_name += ext |
||||
|
attachments.append((report_name, result)) |
||||
|
results[res_id]['attachments'] = attachments |
||||
|
|
||||
|
return multi_mode and results or results[res_ids[0]] |
||||
|
|
||||
|
|
||||
|
@api.multi |
||||
|
def generate_recipients(self, results, res_ids): |
||||
|
"""Generates the recipients of the template. Default values can ben generated |
||||
|
instead of the template values if requested by template or context. |
||||
|
Emails (email_to, email_cc) can be transformed into partners if requested |
||||
|
in the context. """ |
||||
|
self.ensure_one() |
||||
|
|
||||
|
if self.use_default_to or self._context.get('tpl_force_default_to'): |
||||
|
default_recipients = self.env['mail.thread'].message_get_default_recipients(res_model=self.model, res_ids=res_ids) |
||||
|
for res_id, recipients in default_recipients.iteritems(): |
||||
|
results[res_id].pop('partner_to', None) |
||||
|
results[res_id].update(recipients) |
||||
|
|
||||
|
for res_id, values in results.iteritems(): |
||||
|
partner_ids = values.get('partner_ids', list()) |
||||
|
if self._context.get('tpl_partners_only'): |
||||
|
mails = tools.email_split(values.pop('email_to', '')) + tools.email_split(values.pop('email_cc', '')) + tools.email_split(values.pop('email_bcc', '')) |
||||
|
for mail in mails: |
||||
|
partner_id = self.env['res.partner'].find_or_create(mail) |
||||
|
partner_ids.append(partner_id) |
||||
|
partner_to = values.pop('partner_to', '') |
||||
|
if partner_to: |
||||
|
# placeholders could generate '', 3, 2 due to some empty field values |
||||
|
tpl_partner_ids = [int(pid) for pid in partner_to.split(',') if pid] |
||||
|
partner_ids += self.env['res.partner'].sudo().browse(tpl_partner_ids).exists().ids |
||||
|
results[res_id]['partner_ids'] = partner_ids |
||||
|
return results |
||||
|
|
@ -0,0 +1,62 @@ |
|||||
|
<section class="oe_container"> |
||||
|
<div class="oe_row oe_spaced"> |
||||
|
<div class="oe_span12"> |
||||
|
<h2 class="oe_slogan">Mail BCC</h2> |
||||
|
<p>This module was written to add the field Blind Carbon Copy (BCC) to emails and email templates.</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
</section> |
||||
|
|
||||
|
<section class="oe_container oe_dark"> |
||||
|
<div class="oe_row oe_spaced"> |
||||
|
<div class="oe_span12"> |
||||
|
<h2 class="oe_slogan">Installation</h2> |
||||
|
</div> |
||||
|
<div class="oe_span12"> |
||||
|
<p class="oe_mt32">To install this module, you need to: |
||||
|
<ul> |
||||
|
<li>Check that you have installed the module <strong>email_template</strong>.</li> |
||||
|
</ul> |
||||
|
</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
</section> |
||||
|
|
||||
|
<section class="oe_container oe_dark"> |
||||
|
<div class="oe_row oe_spaced"> |
||||
|
<div class="oe_span12"> |
||||
|
<h2 class="oe_slogan">Usage</h2> |
||||
|
</div> |
||||
|
<div class="oe_span12"> |
||||
|
<p class="oe_mt32">To use this module, you need to: |
||||
|
<ul> |
||||
|
<li>Create a new email template from XML code and add fill in the new field <strong>bcc_email</strong>.</li> |
||||
|
<li>Send an email loading the above template from Python code.</li> |
||||
|
</ul> |
||||
|
</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
</section> |
||||
|
|
||||
|
<section class="oe_container oe_dark"> |
||||
|
<div class="oe_row"> |
||||
|
<div class="oe_span12"> |
||||
|
<h2 class="oe_slogan">Credits</h2> |
||||
|
</div> |
||||
|
<div class="oe_span12"> |
||||
|
<h3>Contributors</h3> |
||||
|
<ul> |
||||
|
<li>Juan Formoso <<a href="mailto:jfv@anubia.es">email.jfv@anubia.es</a>></li> |
||||
|
</ul> |
||||
|
</div> |
||||
|
<div class="oe_span12"> |
||||
|
<h3>Maintainer</h3> |
||||
|
<p> |
||||
|
This module is maintained by the OCA.<br/> |
||||
|
OCA, or the Odoo Community Association, is a nonprofit organization whose mission is to support the collaborative development of Odoo features and promote its widespread use.<br/> |
||||
|
To contribute to this module, please visit <a href="http://odoo-community.org">http://odoo-community.org</a>.<br/> |
||||
|
<a href="http://odoo-community.org"><img class="oe_picture oe_centered" src="http://odoo-community.org/logo.png"></a> |
||||
|
</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
</section> |
@ -0,0 +1,15 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||
|
<odoo> |
||||
|
<data> |
||||
|
<record id="mail_bcc_form" model="ir.ui.view"> |
||||
|
<field name="name">mail.template.mail_bcc</field> |
||||
|
<field name="model">mail.template</field> |
||||
|
<field name="inherit_id" ref="mail.email_template_form"/> |
||||
|
<field name="arch" type="xml"> |
||||
|
<xpath expr="/form/sheet/notebook/page[2]/group/field[@name='email_cc']" position="after"> |
||||
|
<field name="email_bcc"/> |
||||
|
</xpath> |
||||
|
</field> |
||||
|
</record> |
||||
|
</data> |
||||
|
</odoo> |
Write
Preview
Loading…
Cancel
Save
Reference in new issue