[IMP] mail: another pass of cleanup/refactoring of mail features - finish renaming

bzr revid: odo@openerp.com-20110722163457-7g9ngdb2p0bixcst
This commit is contained in:
Olivier Dony 2011-07-22 18:34:57 +02:00
parent f9a932fac2
commit c996efa2fe
19 changed files with 1265 additions and 1241 deletions

View File

@ -2,7 +2,7 @@
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
# Copyright (C) 2009-Today OpenERP SA (<http://www.openerp.com>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
@ -19,8 +19,8 @@
#
##############################################################################
import email_message
import email_thread
import mail_message
import mail_thread
import res_partner
import wizard

View File

@ -20,29 +20,35 @@
##############################################################################
{
'name': 'Email System',
'name': 'Email Subsystem',
'version': '1.0',
'category': 'Tools',
'description': """
The generic email system allows to send and receive emails.
===================================================================
A generic email subsystem with message storage and queuing
==========================================================
* SMTP Server Configuration
* Provide API for Sending Messages
* Store all emails releated messages""",
* Uses the global Outgoing Mail Servers for sending mail
* Provides an API for sending messages and archiving them,
grouped by conversation
* Includes queuing mechanism with automated configurable
scheduler-based processing
* Includes a generic mail composition wizard, including
a simple mechanism for mass-mailing with the use of
basic templates - see ``email_template`` module for
more features
""",
'author': 'OpenERP SA',
'website': 'http://www.openerp.com',
'depends': ['base', 'base_tools'],
'init_xml': [],
'update_xml': [
"wizard/email_compose_message_view.xml",
"email_view.xml",
"email_thread_view.xml",
'data': [
"wizard/mail_compose_message_view.xml",
"mail_view.xml",
"mail_thread_view.xml",
"res_partner_view.xml",
'security/ir.model.access.csv',
'email_data.xml',
'mail_data.xml',
],
'demo_xml': [],
'installable': True,
'active': False,
'certificate': '001056784984222247309',

View File

@ -1,538 +0,0 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>)
#
# 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/>
#
##############################################################################
from osv import osv
from osv import fields
from tools.translate import _
import tools
import netsvc
import base64
import time
import logging
import re
import email
from email.header import decode_header
#import binascii
#import email
#from email.header import decode_header
#from email.utils import parsedate
#import base64
#import re
#import logging
#import xmlrpclib
#import re
#import smtplib
#import base64
#from email import Encoders
#from email.mime.base import MIMEBase
#from email.mime.multipart import MIMEMultipart
#from email.mime.text import MIMEText
#from email.header import decode_header, Header
#from email.utils import formatdate
#import netsvc
#import datetime
#import tools
#import logging
LOGGER = netsvc.Logger()
_logger = logging.getLogger('mail')
def format_date_tz(date, tz=None):
if not date:
return 'n/a'
format = tools.DEFAULT_SERVER_DATETIME_FORMAT
return tools.server_to_local_timestamp(date, format, format, tz)
class email_message_common(osv.osv_memory):
_name = 'email.message.common'
_columns = {
'subject':fields.char('Subject', size=512),
'model': fields.char('Object Name', size=128, select=1),
'res_id': fields.integer('Resource ID', select=1),
'date': fields.datetime('Date'),
'user_id': fields.many2one('res.users', 'User Responsible'),
'email_from': fields.char('From', size=128, help='Email From'),
'email_to': fields.char('To', size=256, help='Email Recipients'),
'email_cc': fields.char('Cc', size=256, help='Carbon Copy Email Recipients'),
'email_bcc': fields.char('Bcc', size=256, help='Blind Carbon Copy Email Recipients'),
'email_reply_to':fields.char('Reply-To', size=256),
'headers': fields.text('x_headers'),
'message_id': fields.char('Message Id', size=256, help='Message Id on Email.', select=1),
'references': fields.text('References', help='References emails.'),
'body_text': fields.text('Description'),
'body_html': fields.text('HTML', help="Contains HTML version of email"),
'original': fields.text('Original Email'),
}
_rec_name = 'subject'
_sql_constraints = []
email_message_common()
class email_message(osv.osv):
'''
Email Message
'''
_inherit = 'email.message.common'
_name = 'email.message'
_description = 'Email Message'
_order = 'date desc'
def _check_email_recipients(self, cr, uid, ids, context=None):
'''
checks email_to, email_cc, email_bcc
'''
for message in self.browse(cr, uid, ids, context=context):
if not (message.email_to or message.email_cc or message.email_bcc) and message.history:
return False
return True
_constraints = [
(_check_email_recipients, 'No recipients were specified. Please enter a recipient!', ['email_to', 'email_cc', 'email_bcc']),
]
def open_document(self, cr, uid, ids, context=None):
""" To Open Document
@param self: The object pointer.
@param cr: A database cursor
@param uid: ID of the user currently logged in
@param ids: the ID of messages
@param context: A standard dictionary
"""
action_data = False
if ids:
message_id = ids[0]
mailgate_data = self.browse(cr, uid, message_id, context=context)
model = mailgate_data.model
res_id = mailgate_data.res_id
action_pool = self.pool.get('ir.actions.act_window')
action_ids = action_pool.search(cr, uid, [('res_model', '=', model)])
if action_ids:
action_data = action_pool.read(cr, uid, action_ids[0], context=context)
action_data.update({
'domain' : "[('id','=',%d)]"%(res_id),
'nodestroy': True,
'context': {}
})
return action_data
def open_attachment(self, cr, uid, ids, context=None):
""" To Open attachments
@param self: The object pointer.
@param cr: A database cursor
@param uid: ID of the user currently logged in
@param ids: the ID of messages
@param context: A standard dictionary
"""
action_data = False
action_pool = self.pool.get('ir.actions.act_window')
message_pool = self.browse(cr, uid, ids, context=context)[0]
att_ids = [x.id for x in message_pool.attachment_ids]
action_ids = action_pool.search(cr, uid, [('res_model', '=', 'ir.attachment')])
if action_ids:
action_data = action_pool.read(cr, uid, action_ids[0], context=context)
action_data.update({
'domain': [('id','in',att_ids)],
'nodestroy': True
})
return action_data
def truncate_data(self, cr, uid, data, context=None):
data_list = data and data.split('\n') or []
if len(data_list) > 3:
res = '\n\t'.join(data_list[:3]) + '...'
else:
res = '\n\t'.join(data_list)
return res
def _get_display_text(self, cr, uid, ids, name, arg, context=None):
if context is None:
context = {}
tz = context.get('tz')
result = {}
for message in self.browse(cr, uid, ids, context=context):
msg_txt = ''
if message.history:
msg_txt += _('%s wrote on %s: \n Subject: %s \n\t') % (message.email_from or '/', format_date_tz(message.date, tz), message.subject)
if message.body:
msg_txt += self.truncate_data(cr, uid, message.body, context=context)
else:
msg_txt = (message.user_id.name or '/') + _(' on ') + format_date_tz(message.date, tz) + ':\n\t'
msg_txt += message.subject
result[message.id] = msg_txt
return result
_columns = {
'partner_id': fields.many2one('res.partner', 'Partner'),
'attachment_ids': fields.many2many('ir.attachment', 'message_attachment_rel', 'message_id', 'attachment_id', 'Attachments'),
'display_text': fields.function(_get_display_text, method=True, type='text', size="512", string='Display Text'),
'history': fields.boolean('History', readonly=True),
'state':fields.selection([
('outgoing', 'Outgoing'),
('sent', 'Sent'),
('received', 'Received'),
('exception', 'Exception'),
('cancel', 'Cancelled'),
], 'State', readonly=True),
'auto_delete': fields.boolean('Auto Delete', help="Permanently delete emails after sending"),
'smtp_server_id':fields.many2one('ir.mail_server', 'SMTP Server'),
}
_defaults = {
}
def init(self, cr):
cr.execute("""SELECT indexname
FROM pg_indexes
WHERE indexname = 'email_message_res_id_model_idx'""")
if not cr.fetchone():
cr.execute("""CREATE INDEX email_message_res_id_model_idx
ON email_message (model, res_id)""")
def schedule_with_attach(self, cr, uid, email_from, email_to, subject, body, model=False, email_cc=None, email_bcc=None, reply_to=False, attach=None,
message_id=False, references=False, openobject_id=False, debug=False, subtype='plain', x_headers={}, priority='3', smtp_server_id=False, context=None, auto_delete=False):
if context is None:
context = {}
if attach is None:
attach = {}
attachment_obj = self.pool.get('ir.attachment')
if email_to and type(email_to) != list:
email_to = [email_to]
if email_cc and type(email_cc) != list:
email_cc = [email_cc]
if email_bcc and type(email_bcc) != list:
email_bcc = [email_bcc]
msg_vals = {
'subject': subject,
'model': model or '',
'date': time.strftime('%Y-%m-%d %H:%M:%S'),
'user_id': uid,
'body': body,
'email_from': email_from,
'email_to': email_to and ','.join(email_to) or '',
'email_cc': email_cc and ','.join(email_cc) or '',
'email_bcc': email_bcc and ','.join(email_bcc) or '',
'reply_to': reply_to,
'res_id': openobject_id,
'message_id': message_id,
'references': references or '',
'sub_type': subtype or '',
'headers': x_headers or False,
'priority': priority,
'debug': debug,
'history': True,
'smtp_server_id': smtp_server_id,
'state': 'outgoing',
'auto_delete': auto_delete
}
email_msg_id = self.create(cr, uid, msg_vals, context)
attachment_ids = []
for fname, fcontent in attach.items():
attachment_data = {
'name': fname,
'subject': (subject or '') + _(' (Email Attachment)'),
'datas': fcontent,
'datas_fname': fname,
'body': subject or _('No Description'),
'res_model':'email.message',
'res_id': email_msg_id,
}
if context.has_key('default_type'):
del context['default_type']
attachment_ids.append(attachment_obj.create(cr, uid, attachment_data, context))
self.write(cr, uid, email_msg_id,
{ 'attachment_ids': [[6, 0, attachment_ids]] }, context)
return email_msg_id
def process_retry(self, cr, uid, ids, context=None):
return self.write(cr, uid, ids, {'state':'outgoing'}, context)
def process_email_queue(self, cr, uid, ids=None, context=None):
if ids is None:
ids = []
if context is None:
context = {}
if not ids:
filters = [('state', '=', 'outgoing')]
if 'filters' in context:
filters.extend(context['filters'])
ids = self.search(cr, uid, filters, context=context)
try:
res = self.send_email(cr, uid, ids, auto_commit=True, context=context)
except Exception, error:
logger = netsvc.Logger()
msg = _("Sending of Mail failed. Error: %s") % (error)
logger.notifyChannel("email", netsvc.LOG_ERROR, msg)
return False
return res
def _decode_header(self, text):
"""Returns unicode() string conversion of the the given encoded smtp header"""
if text:
text = decode_header(text.replace('\r', ''))
return ''.join([tools.ustr(x[0], x[1]) for x in text])
def to_email(self, text):
return re.findall(r'([^ ,<@]+@[^> ,]+)', text)
def parse_message(self, message):
"""Return Dictionary Object after parse EML Message String
@param message: email.message.Message object or string or unicode object
"""
msg_txt = message
if isinstance(message, str):
msg_txt = email.message_from_string(message)
# Warning: message_from_string doesn't always work correctly on unicode,
# we must use utf-8 strings here :-(
if isinstance(message, unicode):
message = message.encode('utf-8')
msg_txt = email.message_from_string(message)
message_id = msg_txt.get('message-id', False)
msg = {}
if not message_id:
# Very unusual situation, be we should be fault-tolerant here
message_id = time.time()
msg_txt['message-id'] = message_id
_logger.info('Parsing Message without message-id, generating a random one: %s', message_id)
fields = msg_txt.keys()
msg['id'] = message_id
msg['message-id'] = message_id
if 'Subject' in fields:
msg['subject'] = self._decode_header(msg_txt.get('Subject'))
if 'Content-Type' in fields:
msg['content-type'] = msg_txt.get('Content-Type')
if 'From' in fields:
msg['from'] = self._decode_header(msg_txt.get('From') or msg_txt.get_unixfrom())
if 'Delivered-To' in fields:
msg['to'] = self._decode_header(msg_txt.get('Delivered-To'))
if 'CC' in fields:
msg['cc'] = self._decode_header(msg_txt.get('CC'))
if 'Reply-To' in fields:
msg['reply'] = self._decode_header(msg_txt.get('Reply-To'))
if 'Date' in fields:
msg['date'] = self._decode_header(msg_txt.get('Date'))
if 'Content-Transfer-Encoding' in fields:
msg['encoding'] = msg_txt.get('Content-Transfer-Encoding')
if 'References' in fields:
msg['references'] = msg_txt.get('References')
if 'In-Reply-To' in fields:
msg['in-reply-to'] = msg_txt.get('In-Reply-To')
if 'X-Priority' in fields:
msg['priority'] = priorities[msg_txt.get('X-Priority')]
else:
msg['priority'] = priorities['3 (Normal)']
msg['headers'] = {}
for item in msg_txt.items():
if item[0].startswith('X-'):
msg['headers'].update({item[0]: item[1]})
if not msg_txt.is_multipart() or 'text/plain' in msg.get('content-type', ''):
encoding = msg_txt.get_content_charset()
body = msg_txt.get_payload(decode=True)
if 'text/html' in msg.get('content-type', ''):
msg['body_html'] = body
msg['sub_type'] = 'html'
body = tools.html2plaintext(body)
else:
msg['sub_type'] = 'plain'
msg['body'] = tools.ustr(body, encoding)
attachments = {}
if msg_txt.is_multipart() or 'multipart/alternative' in msg.get('content-type', ''):
body = ""
if 'multipart/alternative' in msg.get('content-type', ''):
msg['sub_type'] = 'alternative'
else:
msg['sub_type'] = 'mixed'
for part in msg_txt.walk():
if part.get_content_maintype() == 'multipart':
continue
encoding = part.get_content_charset()
filename = part.get_filename()
if part.get_content_maintype()=='text':
content = part.get_payload(decode=True)
if filename:
attachments[filename] = content
content = tools.ustr(content, encoding)
if part.get_content_subtype() == 'html':
msg['body_html'] = content
body = tools.ustr(tools.html2plaintext(content))
elif part.get_content_subtype() == 'plain':
body = content
elif part.get_content_maintype() in ('application', 'image'):
if filename :
attachments[filename] = part.get_payload(decode=True)
else:
res = part.get_payload(decode=True)
body += tools.ustr(res, encoding)
msg['body'] = body
msg['attachments'] = attachments
return msg
def send_email(self, cr, uid, ids, auto_commit=False, context=None):
"""
send email message
"""
if context is None:
context = {}
smtp_server_obj = self.pool.get('ir.mail_server')
attachment_pool = self.pool.get('ir.attachment')
self.write(cr, uid, ids, {'state':'outgoing'}, context)
for message in self.browse(cr, uid, ids, context):
try:
smtp_server = message.smtp_server_id
if not smtp_server:
smtp_ids = smtp_server_obj.search(cr, uid, [])
if smtp_ids:
smtp_server = smtp_server_obj.browse(cr, uid, smtp_ids, context)[0]
attachments = []
for attach in message.attachment_ids:
attachments.append((attach.datas_fname, base64.b64decode(attach.datas)))
if message.state in ['outgoing', 'exception']:
msg = smtp_server_obj.pack_message(cr, uid, message.email_from,
message.email_to and message.email_to.split(',') or [], message.subject, message.body,
email_cc=message.email_cc and message.email_cc.split(',') or [],
email_bcc=message.email_bcc and message.email_bcc.split(',') or [],
reply_to=message.reply_to,
attach=attachments, message_id=message.message_id, references = message.references,
openobject_id=message.res_id,
subtype=message.sub_type,
x_headers=message.headers and eval(message.headers) or {},
priority=message.priority)
res = smtp_server_obj.send_email(cr, uid,
msg,
mail_server_id = message.smtp_server_id.id or None,
smtp_server=smtp_server and smtp_server.smtp_host or None,
smtp_port=smtp_server and smtp_server.smtp_port or None,
smtp_user=smtp_server and smtp_server.smtp_user or None,
smtp_password=smtp_server and smtp_server.smtp_pass or None,
ssl=smtp_server and smtp_server.smtp_ssl or False,
tls=smtp_server and smtp_server.smtp_tls,
debug=message.debug)
if res:
self.write(cr, uid, [message.id], {'state':'sent', 'message_id': res}, context)
else:
self.write(cr, uid, [message.id], {'state':'exception'}, context)
else:
raise osv.except_osv(_('Error !'), _('No messages in outgoing or exception state!'))
#if auto_delete=True then delete that sent messages as well as attachments
message_data = self.read(cr, uid, message.id, ['state', 'auto_delete', 'attachment_ids'])
if message_data['state'] == 'sent' and message_data['auto_delete'] == True:
self.unlink(cr, uid, [message.id], context=context)
if message_data['attachment_ids']:
attachment_pool.unlink(cr, uid, message_data['attachment_ids'], context=context)
if auto_commit == True:
cr.commit()
except Exception, error:
logger = netsvc.Logger()
logger.notifyChannel("email-template", netsvc.LOG_ERROR, _("Sending of Mail %s failed. Probable Reason:Could not login to server\nError: %s") % (message.id, error))
self.write(cr, uid, [message.id], {'state':'exception'}, context)
return False
return True
def do_cancel(self, cr, uid, ids, context=None):
'''
Cancel the email to be send
'''
self.write(cr, uid, ids, {'state':'cancel'}, context)
return True
# OLD Code.
# def send_all_mail(self, cr, uid, ids=None, context=None):
# if ids is None:
# ids = []
# if context is None:
# context = {}
# filters = [('folder', '=', 'outbox'), ('state', '!=', 'sending')]
# if 'filters' in context.keys():
# for each_filter in context['filters']:
# filters.append(each_filter)
# ids = self.search(cr, uid, filters, context=context)
# self.write(cr, uid, ids, {'state':'sending'}, context)
# self.send_this_mail(cr, uid, ids, context)
# return True
#
# def send_this_mail(self, cr, uid, ids=None, context=None):
# #previous method to send email (link with email account can be found at the revision 4172 and below
# result = True
# attachment_pool = self.pool.get('ir.attachment')
# for id in (ids or []):
# try:
# account_obj = self.pool.get('email.smtp_server')
# values = self.read(cr, uid, id, [], context)
# payload = {}
# if values['attachments_ids']:
# for attid in values['attachments_ids']:
# attachment = attachment_pool.browse(cr, uid, attid, context)#,['datas_fname','datas'])
# payload[attachment.datas_fname] = attachment.datas
# result = account_obj.send_email(cr, uid,
# [values['account_id'][0]],
# {'To':values.get('email_to') or u'',
# 'CC':values.get('email_cc') or u'',
# 'BCC':values.get('email_bcc') or u'',
# 'Reply-To':values.get('reply_to') or u''},
# values['subject'] or u'',
# {'text':values.get('body_text') or u'', 'html':values.get('body_html') or u''},
# payload=payload,
# message_id=values['message_id'],
# context=context)
# if result == True:
# account = account_obj.browse(cr, uid, values['account_id'][0], context=context)
# if account.auto_delete:
# self.write(cr, uid, id, {'folder': 'trash'}, context=context)
# self.unlink(cr, uid, [id], context=context)
# # Remove attachments for this mail
# attachment_pool.unlink(cr, uid, values['attachments_ids'], context=context)
# else:
# self.write(cr, uid, id, {'folder':'sent', 'state':'na', 'date_mail':time.strftime("%Y-%m-%d %H:%M:%S")}, context)
# else:
# error = result['error_msg']
#
# except Exception, error:
# logger = netsvc.Logger()
# logger.notifyChannel("email-template", netsvc.LOG_ERROR, _("Sending of Mail %s failed. Probable Reason:Could not login to server\nError: %s") % (id, error))
# self.write(cr, uid, id, {'state':'na'}, context)
# return result
email_message()
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -1,388 +0,0 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>)
#
# 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/>
#
##############################################################################
from osv import osv, fields
import time
import tools
import binascii
import email
from email.utils import parsedate
from tools.translate import _
import logging
import xmlrpclib
_logger = logging.getLogger('mail')
class email_thread(osv.osv):
'''
Email Thread
'''
_name = 'email.thread'
_description = 'Email Thread'
_columns = {
'message_ids': fields.one2many('email.message', 'res_id', 'Messages', readonly=True),
}
def copy(self, cr, uid, id, default=None, context=None):
"""
Overrides orm copy method.
@param self: the object pointer
@param cr: the current row, from the database cursor,
@param uid: the current users ID for security checks,
@param id: Id of mailgate thread
@param default: Dictionary of default values for copy.
@param context: A standard dictionary for contextual values
"""
if default is None:
default = {}
default.update({
'message_ids': [],
})
return super(email_thread, self).copy(cr, uid, id, default, context=context)
def message_new(self, cr, uid, msg, context=None):
"""
Called by process_email() to create a new record
corresponding to an incoming message for a new thread.
@param msg: Dictionary Object to contain email message data
"""
if context is None:
context = {}
model = context.get('thread_model', False)
if not model:
model = self._name
model_pool = self.pool.get(model)
fields = model_pool.fields_get(cr, uid, context=context)
data = model_pool.default_get(cr, uid, fields, context=context)
if 'name' in fields and not data.get('name', False):
data['name'] = msg.get('from','')
res_id = model_pool.create(cr, uid, data, context=context)
attachments = msg.get('attachments', {})
self.history(cr, uid, [res_id], _('receive'), history=True,
subject = msg.get('subject'),
email = msg.get('to'),
details = msg.get('body'),
email_from = msg.get('from'),
email_cc = msg.get('cc'),
message_id = msg.get('message-id'),
references = msg.get('references', False) or msg.get('in-reply-to', False),
attach = attachments,
email_date = msg.get('date'),
body_html= msg.get('body_html', False),
sub_type = msg.get('sub_type', False),
headers = msg.get('headers', False),
reply = msg.get('reply', False),
priority = msg.get('priority'),
context = context)
return res_id
def message_update(self, cr, uid, ids, msg, vals={}, default_act=None, context=None):
"""
Called by process_email() to add a new incoming message for an existing thread
@param msg: Dictionary Object to contain email message data
"""
if context is None:
context = {}
model = context.get('thread_model', False)
if not model:
model = self._name
model_pool = self.pool.get(model)
attachments = msg.get('attachments', {})
self.history(cr, uid, ids, _('receive'), history=True,
subject = msg.get('subject'),
email = msg.get('to'),
details = msg.get('body'),
email_from = msg.get('from'),
email_cc = msg.get('cc'),
message_id = msg.get('message-id'),
references = msg.get('references', False) or msg.get('in-reply-to', False),
attach = attachments,
email_date = msg.get('date'),
body_html= msg.get('body_html', False),
sub_type = msg.get('sub_type', False),
headers = msg.get('headers', False),
reply = msg.get('reply', False),
priority = msg.get('priority'),
context = context)
return True
def thread_followers(self, cr, uid, ids, context=None):
""" Get a list of emails of the people following this thread
"""
res = {}
if isinstance(ids, (str, int, long)):
ids = [long(ids)]
for thread in self.browse(cr, uid, ids, context=context):
l = []
for message in thread.message_ids:
l.append((message.user_id and message.user_id.email) or '')
l.append(message.email_from or '')
l.append(message.email_cc or '')
res[thread.id] = l
return res
def history(self, cr, uid, threads, keyword, history=False, subject=None, \
details=None, email=False, email_from=False, email_cc=None, \
email_bcc=None, reply=None, email_date=None, message_id=False, \
references=None, attach=None, body_html=None, sub_type=None, \
headers=None, priority=None, context=None):
"""
@param self: The object pointer
@param cr: the current row, from the database cursor,
@param uid: the current users ID for security checks,
@param threads: a browse record list
@param keyword: Thread action keyword e.g.: If thread is closed "Close" keyword is used
@param history: Value True/False, If True it makes entry as a Emails Messages otherwise Log Messages
@param email: Email-To / Recipient address
@param email_from: Email From / Sender address if any
@param email_cc: Comma-Separated list of Carbon Copy Emails To addresse if any
@param email_bcc: Comma-Separated list of Blind Carbon Copy Emails To addresses if any
@param email_date: Email Date string if different from now, in server Timezone
@param details: Description, Details of thread history if any
@param attach: Attachment sent in email
@param context: A standard dictionary for contextual values"""
if context is None:
context = {}
if attach is None:
attach = {}
if email_date:
edate = parsedate(email_date)
if edate is not None:
email_date = time.strftime('%Y-%m-%d %H:%M:%S', edate)
# The script sends the ids of the threads and not the object list
if all(isinstance(thread_id, (int, long)) for thread_id in threads):
model = context.get('thread_model', False)
if not model:
model = self._name
model_pool = self.pool.get(model)
threads = model_pool.browse(cr, uid, threads, context=context)
att_obj = self.pool.get('ir.attachment')
obj = self.pool.get('email.message')
for thread in threads:
attachments = []
for fname, fcontent in attach.items():
if isinstance(fcontent, unicode):
fcontent = fcontent.encode('utf-8')
data_attach = {
'name': fname,
'datas': binascii.b2a_base64(str(fcontent)),
'datas_fname': fname,
'description': _('Mail attachment'),
'res_model': thread._name,
'res_id': thread.id,
}
attachments.append(att_obj.create(cr, uid, data_attach))
partner_id = hasattr(thread, 'partner_id') and (thread.partner_id and thread.partner_id.id or False) or False
if not partner_id and thread._name == 'res.partner':
partner_id = thread.id
data = {
'subject': keyword,
'user_id': uid,
'model' : thread._name,
'partner_id': partner_id,
'res_id': thread.id,
'date': time.strftime('%Y-%m-%d %H:%M:%S'),
'message_id': message_id,
'body': details or (hasattr(thread, 'description') and thread.description or False),
'attachment_ids': [(6, 0, attachments)]
}
if history:
for param in (email, email_cc, email_bcc):
if isinstance(param, list):
param = ", ".join(param)
data = {
'subject': subject or _('History'),
'history': True,
'user_id': uid,
'model' : thread._name,
'res_id': thread.id,
'date': email_date or time.strftime('%Y-%m-%d %H:%M:%S'),
'body': details,
'email_to': email,
'email_from': email_from or \
(hasattr(thread, 'user_id') and thread.user_id and thread.user_id.address_id and \
thread.user_id.address_id.email),
'email_cc': email_cc,
'email_bcc': email_bcc,
'partner_id': partner_id,
'references': references,
'message_id': message_id,
'attachment_ids': [(6, 0, attachments)],
'state' : 'received',
'body_html': body_html,
'sub_type': sub_type,
'headers': headers,
'reply_to': reply,
'priority': priority
}
obj.create(cr, uid, data, context=context)
return True
def email_forward(self, cr, uid, model, res_ids, msg, email_error=False, context=None):
"""Sends an email to all people following the thread
@param res_id: Id of the record of OpenObject model created from the email message
@param msg: email.message.Message object to forward
@param email_error: Default Email address in case of any Problem
"""
model_pool = self.pool.get(model)
smtp_server_obj = self.pool.get('ir.mail_server')
email_message_obj = self.pool.get('email.message')
_decode_header = email_message_obj._decode_header
for res in model_pool.browse(cr, uid, res_ids, context=context):
if hasattr(model_pool, 'thread_followers'):
self.thread_followers = model_pool.thread_followers
thread_followers = self.thread_followers(cr, uid, [res.id])[res.id]
message_followers_emails = email_message_obj.to_email(','.join(filter(None, thread_followers)))
message_recipients = email_message_obj.to_email(','.join(filter(None,
[_decode_header(msg['from']),
_decode_header(msg['to']),
_decode_header(msg['cc'])])))
message_forward = [i for i in message_followers_emails if (i and (i not in message_recipients))]
if message_forward:
# TODO: we need an interface for this for all types of objects, not just leads
if hasattr(res, 'section_id'):
del msg['reply-to']
msg['reply-to'] = res.section_id.reply_to
smtp_from = email_message_obj.to_email(msg['from'])
msg['from'] = smtp_from
msg['to'] = message_forward
msg['message-id'] = tools.generate_tracking_message_id(res.id)
if not smtp_server_obj.send_email(cr, uid, msg) and email_error:
subj = msg['subject']
del msg['subject'], msg['to'], msg['cc'], msg['bcc']
msg['subject'] = _('[OpenERP-Forward-Failed] %s') % subj
msg['to'] = email_error
smtp_server_obj.send_email(cr, uid, msg)
return True
def process_email(self, cr, uid, model, message, custom_values=None, attach=True, context=None):
"""This function Processes email and create record for given OpenERP model
@param self: The object pointer
@param cr: the current row, from the database cursor,
@param uid: the current users ID for security checks,
@param model: OpenObject Model
@param message: Email details, passed as a string or an xmlrpclib.Binary
@param attach: Email attachments
@param context: A standard dictionary for contextual values"""
# extract message bytes, we are forced to pass the message as binary because
# we don't know its encoding until we parse its headers and hence can't
# convert it to utf-8 for transport between the mailgate script and here.
if isinstance(message, xmlrpclib.Binary):
message = str(message.data)
if context is None:
context = {}
if custom_values is None or not isinstance(custom_values, dict):
custom_values = {}
model_pool = self.pool.get(model)
if self._name != model:
context.update({'thread_model':model})
email_message_pool = self.pool.get('email.message')
res_id = False
# Parse Message
# Warning: message_from_string doesn't always work correctly on unicode,
# we must use utf-8 strings here :-(
if isinstance(message, unicode):
message = message.encode('utf-8')
msg_txt = email.message_from_string(message)
msg = email_message_pool.parse_message(msg_txt)
# Create New Record into particular model
def create_record(msg):
if hasattr(model_pool, 'message_new'):
new_res_id = model_pool.message_new(cr, uid, msg, context=context)
if custom_values:
model_pool.write(cr, uid, [new_res_id], custom_values, context=context)
return new_res_id
res_id = False
if msg.get('references') or msg.get('in-reply-to'):
references = msg.get('references') or msg.get('in-reply-to')
if '\r\n' in references:
references = references.split('\r\n')
else:
references = references.split(' ')
for ref in references:
ref = ref.strip()
res_id = tools.reference_re.search(ref)
if res_id:
res_id = res_id.group(1)
else:
res_id = tools.res_re.search(msg['subject'])
if res_id:
res_id = res_id.group(1)
if res_id:
res_id = int(res_id)
if model_pool.exists(cr, uid, res_id):
if hasattr(model_pool, 'message_update'):
model_pool.message_update(cr, uid, [res_id], msg, {}, context=context)
if not res_id:
res_id = create_record(msg)
#To forward the email to other followers
self.email_forward(cr, uid, model, [res_id], msg_txt)
return res_id
def get_partner(self, cr, uid, from_email, context=None):
"""This function returns partner Id based on email passed
@param self: The object pointer
@param cr: the current row, from the database cursor,
@param uid: the current users ID for security checks
@param from_email: email address based on that function will search for the correct
"""
address_pool = self.pool.get('res.partner.address')
email_message_pool = self.pool.get('email.message')
res = {
'partner_address_id': False,
'partner_id': False
}
from_email = email_message_pool.to_email(from_email)[0]
address_ids = address_pool.search(cr, uid, [('email', 'like', from_email)])
if address_ids:
address = address_pool.browse(cr, uid, address_ids[0])
res['partner_address_id'] = address_ids[0]
res['partner_id'] = address.partner_id.id
return res
email_thread()
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -2,13 +2,13 @@
<openerp>
<data noupdate="1">
<record forcecreate="True" id="ir_cron_mail_scheduler_action" model="ir.cron">
<field name="name">Email scheduler</field>
<field name="name">Outgoing emails robot</field>
<field name="user_id" ref="base.user_root"/>
<field name="interval_number">1</field>
<field name="interval_type">hours</field>
<field name="numbercall">-1</field>
<field eval="False" name="doall"/>
<field eval="'email.message'" name="model"/>
<field eval="'mail.message'" name="model"/>
<field eval="'process_email_queue'" name="function"/>
<field eval="'()'" name="args"/>
</record>

494
addons/mail/mail_message.py Normal file
View File

@ -0,0 +1,494 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2010-2011 OpenERP SA (<http://www.openerp.com>)
#
# 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/>
#
##############################################################################
import base64
import email
import logging
import re
import time
from email.header import decode_header
import tools
from osv import osv
from osv import fields
from tools.translate import _
from tools.safe_eval import literal_eval
_logger = logging.getLogger('mail')
def format_date_tz(date, tz=None):
if not date:
return 'n/a'
format = tools.DEFAULT_SERVER_DATETIME_FORMAT
return tools.server_to_local_timestamp(date, format, format, tz)
def truncate_text(text):
lines = text and text.split('\n') or []
if len(lines) > 3:
res = '\n\t'.join(lines[:3]) + '...'
else:
res = '\n\t'.join(lines)
return res
def decode(text):
"""Returns unicode() string conversion of the the given encoded smtp header text"""
if text:
text = decode_header(text.replace('\r', ''))
return ''.join([tools.ustr(x[0], x[1]) for x in text])
def to_email(text):
"""Return a list of the email addresses found in ``text``"""
if not text: return []
return re.findall(r'([^ ,<@]+@[^> ,]+)', text)
class mail_message_common(osv.osv_memory):
"""Common abstract class for holding the main attributes of a
message object. It could be reused as parent model for any
database model or wizard screen that needs to hold a kind of
message"""
_name = 'mail.message.common'
_rec_name = 'subject'
_columns = {
'subject': fields.char('Subject', size=512, required=True),
'model': fields.char('Related Document model', size=128, select=1, readonly=1),
'res_id': fields.integer('Related Document ID', select=1, readonly=1),
'date': fields.datetime('Date'),
'email_from': fields.char('From', size=128, help='Message sender'),
'email_to': fields.char('To', size=256, help='Message recipients'),
'email_cc': fields.char('Cc', size=256, help='Carbon copy message recipients'),
'email_bcc': fields.char('Bcc', size=256, help='Blind carbon copy message recipients'),
'reply_to':fields.char('Reply-To', size=256, help='Response address for the message'),
'headers': fields.text('Message headers', help="Full message headers, e.g. SMTP session headers", readonly=1),
'message_id': fields.char('Message-Id', size=256, help='Message unique identifier', select=1, readonly=1),
'references': fields.text('References', help='Message references, such as identifiers of previous messages', readonly=1),
'subtype': fields.char('Message type', size=32, help="Type of message, usually 'html' or 'plain', used to "
"select plaintext or rich text contents accordingly", readonly=1),
'body_text': fields.text('Text contents', help="Plain-text version of the message"),
'body_html': fields.text('Rich-text contents', help="Rich-text/HTML version of the message"),
'original': fields.text('Original', help="Original version of the message, before being imported by the system", readonly=1),
}
_defaults = {
'subtype': 'plain'
}
class mail_message(osv.osv):
'''Model holding RFC2822 email messages, and providing facilities
to parse, queue and send new messages
Messages that do not have a value for the email_from column
are simple log messages (e.g. document state changes), while
actual e-mails have the email_from value set.
The ``display_text`` field will have a slightly different
presentation for real emails and for log messages.
'''
_name = 'mail.message'
_inherit = 'mail.message.common'
_description = 'Email Message'
_order = 'date desc'
# XXX to review - how to determine action to use?
def open_document(self, cr, uid, ids, context=None):
action_data = False
if ids:
msg = self.browse(cr, uid, ids[0], context=context)
model = msg.model
res_id = msg.res_id
ir_act_window = self.pool.get('ir.actions.act_window')
action_ids = ir_act_window.search(cr, uid, [('res_model', '=', model)])
if action_ids:
action_data = ir_act_window.read(cr, uid, action_ids[0], context=context)
action_data.update({
'domain' : "[('id','=',%d)]"%(res_id),
'nodestroy': True,
'context': {}
})
return action_data
# XXX to review - how to determine action to use?
def open_attachment(self, cr, uid, ids, context=None):
action_data = False
action_pool = self.pool.get('ir.actions.act_window')
message = self.browse(cr, uid, ids, context=context)[0]
att_ids = [x.id for x in message.attachment_ids]
action_ids = action_pool.search(cr, uid, [('res_model', '=', 'ir.attachment')])
if action_ids:
action_data = action_pool.read(cr, uid, action_ids[0], context=context)
action_data.update({
'domain': [('id','in',att_ids)],
'nodestroy': True
})
return action_data
def _get_display_text(self, cr, uid, ids, name, arg, context=None):
if context is None:
context = {}
tz = context.get('tz')
result = {}
for message in self.browse(cr, uid, ids, context=context):
msg_txt = ''
if message.email_from:
msg_txt += _('%s wrote on %s: \n Subject: %s \n\t') % (message.email_from or '/', format_date_tz(message.date, tz), message.subject)
if message.body:
msg_txt += truncate_text(message.body)
else:
msg_txt = (message.user_id.name or '/') + _(' on ') + format_date_tz(message.date, tz) + ':\n\t'
msg_txt += message.subject
result[message.id] = msg_txt
return result
_columns = {
'partner_id': fields.many2one('res.partner', 'Related partner'),
'user_id': fields.many2one('res.users', 'Related user', readonly=1),
'attachment_ids': fields.many2many('ir.attachment', 'message_attachment_rel', 'message_id', 'attachment_id', 'Attachments'),
'display_text': fields.function(_get_display_text, method=True, type='text', size="512", string='Display Text'),
'mail_server_id': fields.many2one('ir.mail_server', 'Outgoing mail server', readonly=1),
'state': fields.selection([
('outgoing', 'Outgoing'),
('sent', 'Sent'),
('received', 'Received'),
('exception', 'Exception'),
('cancel', 'Cancelled'),
], 'State', readonly=True),
'auto_delete': fields.boolean('Auto Delete', help="Permanently delete this email after sending it"),
}
def init(self, cr):
cr.execute("""SELECT indexname FROM pg_indexes WHERE indexname = 'mail_message_model_res_id_idx'""")
if not cr.fetchone():
cr.execute("""CREATE INDEX mail_message_model_res_id_idx ON mail_message (model, res_id)""")
def schedule_with_attach(self, cr, uid, email_from, email_to, subject, body, model=False, email_cc=None,
email_bcc=None, reply_to=False, attachments=None, message_id=False, references=False,
res_id=False, subtype='plain', headers=None, mail_server_id=False, auto_delete=False,
context=None):
"""Schedule sending a new email message, to be sent the next time the mail scheduler runs, or
the next time :meth:`process_email_queue` is called explicitly.
: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, according to the ``subtype`` (by default, plaintext).
If html subtype is used, the message will be automatically converted
to plaintext and wrapped in multipart/alternative.
: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 string model: optional model name of the document this mail is related to (this will also
be used to generate a tracking id, used to match any response related to the
same document)
:param int res_id: optional resource identifier this mail is related to (this will also
be used to generate a tracking id, used to match any response related to the
same document)
:param string reply_to: optional value of Reply-To header
: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 list attachments: list of (filename, filecontents) pairs, where filecontents is a string
containing the bytes of the attachment
: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.)
:param int mail_server_id: optional id of the preferred outgoing mail server for this mail
:param bool auto_delete: optional flag to turn on auto-deletion of the message after it has been
successfully sent (default to False)
"""
if context is None:
context = {}
if attachments is None:
attachments = {}
attachment_obj = self.pool.get('ir.attachment')
for param in (email_to, email_cc, email_bcc):
if param and not isinstance(param, list):
param = [param]
msg_vals = {
'subject': subject,
'date': time.strftime('%Y-%m-%d %H:%M:%S'),
'user_id': uid,
'model': model,
'res_id': res_id,
'body_text': body if subtype == 'plain' else False,
'body_html': body if subtype == 'html' else False,
'email_from': email_from,
'email_to': email_to and ','.join(email_to) or '',
'email_cc': email_cc and ','.join(email_cc) or '',
'email_bcc': email_bcc and ','.join(email_bcc) or '',
'reply_to': reply_to,
'message_id': message_id,
'references': references,
'subtype': subtype,
'headers': headers, # serialize the dict on the fly
'mail_server_id': mail_server_id,
'state': 'outgoing',
'auto_delete': auto_delete
}
email_msg_id = self.create(cr, uid, msg_vals, context)
attachment_ids = []
for fname, fcontent in attachments.items():
attachment_data = {
'name': fname,
'datas_fname': fname,
'datas': fcontent,
'res_model': self._name,
'res_id': email_msg_id,
}
if context.has_key('default_type'):
del context['default_type']
attachment_ids.append(attachment_obj.create(cr, uid, attachment_data, context))
if attachment_ids:
self.write(cr, uid, email_msg_id, { 'attachment_ids': [(6, 0, attachment_ids)]}, context=context)
return email_msg_id
def mark_outgoing(self, cr, uid, ids, context=None):
return self.write(cr, uid, ids, {'state':'outgoing'}, context)
def process_email_queue(self, cr, uid, ids=None, context=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).
"""
if context is None:
context = {}
if not ids:
filters = [('state', '=', 'outgoing')]
if 'filters' in context:
filters.extend(context['filters'])
ids = self.search(cr, uid, filters, context=context)
res = None
try:
# Force auto-commit - this is meant to be called by
# the scheduler, and we can't allow rolling back the status
# of previously sent emails!
res = self.send(cr, uid, ids, auto_commit=True, context=context)
except Exception:
_logger.exception("Failed processing mail queue")
return res
def parse_message(self, message):
"""Parses a string or email.message.Message representing an
RFC-2822 email, and returns a generic dict holding the
message details.
:param message: the message to parse
:type message: email.message.Message | string | unicode
:rtype: dict
:return: A dict with the following structure, where each
field may not be present if missing in original
message::
{ 'message-id': msg_id,
'subject': subject,
'from': from,
'to': to,
'cc': cc,
'headers' : { 'X-Mailer': mailer,
#.. all X- headers...
},
'subtype': msg_mime_subtype,
'body': plaintext_body
'body_html': html_body,
'attachments': { 'file1': 'bytes',
'file2': 'bytes' }
# ...
'original': source_of_email,
}
"""
msg_txt = message
if isinstance(message, str):
msg_txt = email.message_from_string(message)
# Warning: message_from_string doesn't always work correctly on unicode,
# we must use utf-8 strings here :-(
if isinstance(message, unicode):
message = message.encode('utf-8')
msg_txt = email.message_from_string(message)
message_id = msg_txt.get('message-id', False)
msg = {}
# save original, we need to be able to read the original email sometimes
msg['original'] = message
if not message_id:
# Very unusual situation, be we should be fault-tolerant here
message_id = time.time()
msg_txt['message-id'] = message_id
_logger.info('Parsing Message without message-id, generating a random one: %s', message_id)
fields = msg_txt.keys()
msg['id'] = message_id
msg['message-id'] = message_id
if 'Subject' in fields:
msg['subject'] = decode(msg_txt.get('Subject'))
if 'Content-Type' in fields:
msg['content-type'] = msg_txt.get('Content-Type')
if 'From' in fields:
msg['from'] = decode(msg_txt.get('From') or msg_txt.get_unixfrom())
if 'Delivered-To' in fields:
msg['to'] = decode(msg_txt.get('Delivered-To'))
if 'CC' in fields:
msg['cc'] = decode(msg_txt.get('CC'))
if 'Reply-To' in fields:
msg['reply'] = decode(msg_txt.get('Reply-To'))
if 'Date' in fields:
msg['date'] = decode(msg_txt.get('Date'))
if 'Content-Transfer-Encoding' in fields:
msg['encoding'] = msg_txt.get('Content-Transfer-Encoding')
if 'References' in fields:
msg['references'] = msg_txt.get('References')
if 'In-Reply-To' in fields:
msg['in-reply-to'] = msg_txt.get('In-Reply-To')
msg['headers'] = {}
for item in msg_txt.items():
if item[0].startswith('X-'):
msg['headers'].update({item[0]: item[1]})
if not msg_txt.is_multipart() or 'text/plain' in msg.get('content-type', ''):
encoding = msg_txt.get_content_charset()
body = msg_txt.get_payload(decode=True)
if 'text/html' in msg.get('content-type', ''):
msg['body_html'] = body
msg['subtype'] = 'html'
body = tools.html2plaintext(body)
else:
msg['subtype'] = 'plain'
msg['body'] = tools.ustr(body, encoding)
attachments = {}
if msg_txt.is_multipart() or 'multipart/alternative' in msg.get('content-type', ''):
body = ""
if 'multipart/alternative' in msg.get('content-type', ''):
msg['subtype'] = 'alternative'
else:
msg['subtype'] = 'mixed'
for part in msg_txt.walk():
if part.get_content_maintype() == 'multipart':
continue
encoding = part.get_content_charset()
filename = part.get_filename()
if part.get_content_maintype()=='text':
content = part.get_payload(decode=True)
if filename:
attachments[filename] = content
content = tools.ustr(content, encoding)
if part.get_content_subtype() == 'html':
msg['body_html'] = content
body = tools.ustr(tools.html2plaintext(content))
elif part.get_content_subtype() == 'plain':
body = content
elif part.get_content_maintype() in ('application', 'image'):
if filename :
attachments[filename] = part.get_payload(decode=True)
else:
res = part.get_payload(decode=True)
body += tools.ustr(res, encoding)
msg['body'] = body
msg['attachments'] = attachments
return msg
def send(self, cr, uid, ids, auto_commit=False, context=None):
"""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 message is output in the server logs.
:param bool auto_commit: whether to force a commit of the message
status after sending each message (meant
only for processing by the scheduler),
should never be True during normal
transactions (default: False)
:return: True
"""
if context is None:
context = {}
ir_mail_server = self.pool.get('ir.mail_server')
self.write(cr, uid, ids, {'state': 'outgoing'}, context=context)
for message in self.browse(cr, uid, ids, context=context):
try:
attachments = []
for attach in message.attachment_ids:
attachments.append((attach.datas_fname, base64.b64decode(attach.datas)))
msg = ir_mail_server.build_email(
email_from=message.email_from,
email_to=to_email(message.email_to),
subject=message.subject,
body=message.body_html if message.subtype == 'html' else message.body_text,
email_cc=to_email(message.email_cc),
email_bcc=to_email(message.email_bcc),
reply_to=message.reply_to,
attachments=attachments, message_id=message.message_id,
references = message.references,
object_id=message.res_id and ('%s-%s' % (message.res_id,message.model)),
subtype=message.subtype,
headers=message.headers and literal_eval(message.headers))
res = ir_mail_server.send_email(cr, uid, msg,
mail_server_id=message.mail_server_id.id,
context=context)
if res:
message.write({'state':'sent', 'message_id': res})
else:
message.write({'state':'exception'})
# if auto_delete=True then delete that sent messages as well as attachments
message.refresh()
if message.state == 'sent' and message.auto_delete:
self.pool.get('ir.attachment').unlink(cr, uid,
[x.id for x in message.attachment_ids],
context=context)
message.unlink()
except Exception:
_logger.exception('failed sending mail.message %s', message.id)
message.write({'state':'exception'})
if auto_commit == True:
cr.commit()
return True
def cancel(self, cr, uid, ids, context=None):
self.write(cr, uid, ids, {'state':'cancel'}, context=context)
return True
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

421
addons/mail/mail_thread.py Normal file
View File

@ -0,0 +1,421 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2009-Today OpenERP SA (<http://www.openerp.com>)
#
# 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/>
#
##############################################################################
import time
import tools
import binascii
import email
from email.utils import parsedate
import logging
import xmlrpclib
from osv import osv, fields
from tools.translate import _
from mail_message import decode, to_email
_logger = logging.getLogger('mail')
class mail_thread(osv.osv):
'''Mixin model, meant to be inherited by any model that needs to
act as a discussion topic on which messages can be attached.
mail.thread adds a one2many of mail.messages, acting as thread
history, and a few methods that may be overridden to implement
specific behavior.
'''
_name = 'mail.thread'
_description = 'Email Thread'
_columns = {
'message_ids': fields.one2many('mail.message', 'res_id', 'Messages', readonly=True),
}
def thread_followers(self, cr, uid, ids, context=None):
"""Returns a list of email addresses of the people following
this thread, including the sender of each mail, and the
people who were in CC of the messages, if any.
"""
res = {}
if isinstance(ids, (str, int, long)):
ids = [long(ids)]
for thread in self.browse(cr, uid, ids, context=context):
l = set()
for message in thread.message_ids:
l.add((message.user_id and message.user_id.email) or '')
l.add(message.email_from or '')
l.add(message.email_cc or '')
res[thread.id] = filter(None, l)
return res
def copy(self, cr, uid, id, default=None, context=None):
"""Overrides default copy method to empty the thread of
messages attached to this record, as the copied object
will have its own thread and does not have to share it.
"""
if default is None:
default = {}
default.update({
'message_ids': [],
})
return super(mail_thread, self).copy(cr, uid, id, default, context=context)
def message_new(self, cr, uid, msg_dict, custom_values=None, context=None):
"""Called by ``process_email`` when a new message is received
without referencing an existing thread. The default
behavior is to create a new record of the corresponding
model, then call ``append_mail()`` to attach a new
mail.message to the newly created record.
Additional behavior may be implemented by overriding this method.
:param dict msg_dict: a map containing the email details and
attachments. See ``process_email`` and
``mail.message.parse()`` for details.
:param dict custom_values: optional dictionary of additional
field values to pass to create()
when creating the new thread record.
Be careful, these values may override
any other values coming from the message.
:param dict context: if a ``thread_model`` value is present
in the context, its value will be used
to determine the model of the record
to create (instead of the current model).
:rtype: int
:return: the id of the newly created thread object
"""
if context is None:
context = {}
model = context.get('thread_model') or self._name
model_pool = self.pool.get(model)
fields = model_pool.fields_get(cr, uid, context=context)
data = model_pool.default_get(cr, uid, fields, context=context)
if 'name' in fields and not data.get('name'):
data['name'] = msg_dict.get('from','')
if custom_values and isinstance(custom_values, dict):
data.update(custom_values)
res_id = model_pool.create(cr, uid, data, context=context)
self.append_mail(cr, uid, ids, msg_dict, context=context)
return res_id
def message_update(self, cr, uid, ids, msg_dict, vals={}, default_act=None, context=None):
"""Called by ``process_email`` when a new message is received
for an existing thread. The default behavior is to create a
new mail.message in the given thread by calling
``append_mail()``.
Additional behavior may be implemented by overriding this
method.
:param dict msg_dict: a map containing the email details and
attachments. See ``process_email`` and
``mail.message.parse()`` for details.
:param dict context: if a ``thread_model`` value is present
in the context, its value will be used
to determine the model of the thread to
update (instead of the current model).
"""
return self.append_mail(cr, uid, ids, msg_dict, context=context)
def append_mail(self, cr, uid, ids, msg_dict, context=None):
"""Creates a new mail.message attached to the given threads,
with the contents of msg_dict, by calling ``history`` with
the mail details. All attachments in msg_dict will be
attached to the thread record as well as to the actual
message.
:param dict msg_dict: a map containing the email details and
attachments. See ``process_email`` and
``mail.message.parse()`` for details.
:param dict context: if a ``thread_model`` value is present
in the context, its value will be used
to determine the model of the thread to
update (instead of the current model).
"""
return self.history(cr, uid, ids,
subject = msg_dict.get('subject'),
body_text = msg_dict.get('body'),
email_to = msg_dict.get('to'),
email_from = msg_dict.get('from'),
email_cc = msg_dict.get('cc'),
email_bcc = msg_dict.get('bcc'),
reply_to = msg_dict.get('reply'),
email_date = msg_dict.get('date'),
message_id = msg_dict.get('message-id'),
references = msg_dict.get('references')\
or msg_dict.get('in-reply-to'),
attachments = attachments,
body_html= msg_dict.get('body_html'),
subtype = msg_dict.get('subtype'),
headers = msg_dict.get('headers'),
original = msg_dict.get('original'),
context = context)
def history(self, cr, uid, threads, subject, body_text=None, email_to=False,
email_from=False, email_cc=None, email_bcc=None, reply_to=None,
email_date=None, message_id=False, references=None,
attachments=None, body_html=None, subtype=None, headers=None,
original=None, context=None):
"""Creates a new mail.message attached to the current mail.thread,
containing all the details passed as parameters. All attachments
will be attached to the thread record as well as to the actual
message.
:param threads: list of thread ids, or list of browse_records representing
threads to which a new message should be attached
:param subject: Thread action keyword e.g.: If thread is closed "Close" keyword is used
:param email_to: Email-To / Recipient address
:param email_from: Email From / Sender address if any
:param email_cc: Comma-Separated list of Carbon Copy Emails To addresse if any
:param email_bcc: Comma-Separated list of Blind Carbon Copy Emails To addresses if any
:param reply_to: reply_to header
:param email_date: email date string if different from now, in server timezone
:param message_id: optional email identifier
:param references: optional email references
:param body_text: plaintext contents of the mail or log message
:param body_html: html contents of the mail or log message
:param subtype: optional type of message: 'plain' or 'html', corresponding to the main
body contents (body_text or body_html).
:param headers: mail headers to store
:param dict attachments: map of attachment filenames to binary contents, if any.
:param str original: optional full source of the RFC2822 email, for reference
:param dict context: if a ``thread_model`` value is present
in the context, its value will be used
to determine the model of the thread to
update (instead of the current model).
"""
if context is None:
context = {}
if attachments is None:
attachments = {}
if email_date:
edate = parsedate(email_date)
if edate is not None:
email_date = time.strftime('%Y-%m-%d %H:%M:%S', edate)
if all(isinstance(thread_id, (int, long)) for thread_id in threads):
model = context.get('thread_model') or self._name
model_pool = self.pool.get(model)
threads = model_pool.browse(cr, uid, threads, context=context)
ir_attachment = self.pool.get('ir.attachment')
mail_message = self.pool.get('mail.message')
for thread in threads:
to_attach = []
for fname, fcontent in attachments.items():
if isinstance(fcontent, unicode):
fcontent = fcontent.encode('utf-8')
data_attach = {
'name': fname,
'datas': binascii.b2a_base64(str(fcontent)),
'datas_fname': fname,
'description': _('Mail attachment'),
'res_model': thread._name,
'res_id': thread.id,
}
to_attach.append(ir_attachment.create(cr, uid, data_attach, context=context))
partner_id = hasattr(thread, 'partner_id') and (thread.partner_id and thread.partner_id.id or False) or False
if not partner_id and thread._name == 'res.partner':
partner_id = thread.id
data = {
'subject': subject,
'user_id': uid,
'model' : thread._name,
'partner_id': partner_id,
'res_id': thread.id,
'date': time.strftime('%Y-%m-%d %H:%M:%S'),
'message_id': message_id,
'body_text': body_text or (hasattr(thread, 'description') and thread.description or False),
'attachment_ids': [(6, 0, to_attach)]
}
if email_from:
for param in (email_to, email_cc, email_bcc):
if isinstance(param, list):
param = ", ".join(param)
data = {
'subject': subject or _('History'),
'user_id': uid,
'model' : thread._name,
'res_id': thread.id,
'date': email_date or time.strftime('%Y-%m-%d %H:%M:%S'),
'body_text': body_text,
'email_to': email_to,
'email_from': email_from or \
(hasattr(thread, 'user_id') and thread.user_id and thread.user_id.address_id and \
thread.user_id.address_id.email),
'email_cc': email_cc,
'email_bcc': email_bcc,
'partner_id': partner_id,
'references': references,
'message_id': message_id,
'attachment_ids': [(6, 0, to_attach)],
'state' : 'received',
'body_html': body_html,
'subtype': subtype,
'headers': headers,
'reply_to': reply_to,
'original': original,
}
mail_message.create(cr, uid, data, context=context)
return True
def process_email(self, cr, uid, model, message, custom_values=None, context=None):
"""Process an incoming RFC2822 email message related to the
given thread model, relying on ``mail.message.parse()``
for the parsing operation, and then calling ``message_new``
(if the thread record did not exist) or ``message_update``
(if it did), then calling ``email_forward()`` to automatically
notify other people that should receive this email.
:param string model: the thread model for which a new message
must be processed
:param message: source of the RFC2822 mail
:type message: string or xmlrpclib.Binary
:type dict custom_value: optional dictionary of field values
to pass to ``message_new`` if a new
record needs to be created. Ignored
if the thread record already exists.
"""
# extract message bytes - we are forced to pass the message as binary because
# we don't know its encoding until we parse its headers and hence can't
# convert it to utf-8 for transport between the mailgate script and here.
if isinstance(message, xmlrpclib.Binary):
message = str(message.data)
model_pool = self.pool.get(model)
if self._name != model:
if context is None: context = {}
context.update({'thread_model':model})
mail_message = self.pool.get('mail.message')
res_id = False
# Parse Message
# Warning: message_from_string doesn't always work correctly on unicode,
# we must use utf-8 strings here :-(
if isinstance(message, unicode):
message = message.encode('utf-8')
msg_txt = email.message_from_string(message)
msg = mail_message.parse_message(msg_txt)
# Create New Record into particular model
def create_record(msg):
if hasattr(model_pool, 'message_new'):
return model_pool.message_new(cr, uid, msg,
custom_values,
context=context)
res_id = False
if msg.get('references') or msg.get('in-reply-to'):
references = msg.get('references') or msg.get('in-reply-to')
if '\r\n' in references:
references = references.split('\r\n')
else:
references = references.split(' ')
for ref in references:
ref = ref.strip()
res_id = tools.reference_re.search(ref)
if res_id:
res_id = res_id.group(1)
else:
res_id = tools.res_re.search(msg['subject'])
if res_id:
res_id = res_id.group(1)
if res_id:
res_id = int(res_id)
if model_pool.exists(cr, uid, res_id):
if hasattr(model_pool, 'message_update'):
model_pool.message_update(cr, uid, [res_id], msg, {}, context=context)
if not res_id:
res_id = create_record(msg)
#To forward the email to other followers
self.email_forward(cr, uid, model, [res_id], msg_txt, context=context)
return res_id
def email_forward(self, cr, uid, model, thread_ids, msg, email_error=False, context=None):
"""Sends an email to all people following the given threads.
The emails are forwarded immediately, not queued for sending,
and not archived.
:param str model: thread model
:param list thread_ids: ids of the thread records
:param msg: email.message.Message object to forward
:param email_error: optional email address to notify in case
of any delivery error during the forward.
:return: True
"""
model_pool = self.pool.get(model)
smtp_server_obj = self.pool.get('ir.mail_server')
mail_message = self.pool.get('mail.message')
for res in model_pool.browse(cr, uid, thread_ids, context=context):
if hasattr(model_pool, 'thread_followers'):
self.thread_followers = model_pool.thread_followers
thread_followers = self.thread_followers(cr, uid, [res.id])[res.id]
message_followers_emails = mail_message.to_email(','.join(filter(None, thread_followers)))
message_recipients = mail_message.to_email(','.join(filter(None,
[decode(msg['from']),
decode(msg['to']),
decode(msg['cc'])])))
message_forward = [i for i in message_followers_emails if (i and (i not in message_recipients))]
if message_forward:
# TODO: we need an interface for this for all types of objects, not just leads
if hasattr(res, 'section_id'):
del msg['reply-to']
msg['reply-to'] = res.section_id.reply_to
smtp_from = mail_message.to_email(msg['from'])
msg['from'] = smtp_from
msg['to'] = message_forward
msg['message-id'] = tools.generate_tracking_message_id(res.id)
if not smtp_server_obj.send_email(cr, uid, msg) and email_error:
subj = msg['subject']
del msg['subject'], msg['to'], msg['cc'], msg['bcc']
msg['subject'] = _('[OpenERP-Forward-Failed] %s') % subj
msg['to'] = email_error
smtp_server_obj.send_email(cr, uid, msg)
return True
def get_partner(self, cr, uid, email, context=None):
"""Attempts to return the id of a partner address matching
the given ``email``, and the corresponding partner.
:param email: email address for which a partner
should be searched for.
:rtype: dict
:return: a map of the following form::
{ 'partner_address_id': id or False,
'partner_id': pid or False }
"""
address_pool = self.pool.get('res.partner.address')
res = {
'partner_address_id': False,
'partner_id': False
}
email = to_email(email)[0]
address_ids = address_pool.search(cr, uid, [('email', '=', email)])
if address_ids:
address = address_pool.browse(cr, uid, address_ids[0])
res['partner_address_id'] = address_ids[0]
res['partner_id'] = address.partner_id.id
return res
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -3,22 +3,21 @@
<data>
<record model="ir.ui.view" id="view_mailgate_thread_form">
<field name="name">email.thread.form</field>
<field name="model">email.thread</field>
<field name="name">mail.thread.form</field>
<field name="model">mail.thread</field>
<field name="type">form</field>
<field name="arch" type="xml">
<form string="Mailgateway Thread">
<separator string="History" colspan="4"/>
<form string="Email Thread">
<separator string="Communication History" colspan="4"/>
<field name="message_ids" nolabel="1" colspan="4" mode="tree,form">
<tree string="Mailgateway History">
<tree string="Communication History">
<field name="display_text"/>
</tree>
<form string="Mailgate History">
<form string="Message">
<field name="subject" widget="char"/>
<field name="date"/>
<field name="user_id"/>
<field name="message_id"/>
<field name="history"/>
<notebook colspan="4">
<page string="Email Details">
<group col="4" colspan="4">
@ -41,38 +40,24 @@
</record>
<record model="ir.ui.view" id="view_mailgate_thread_tree">
<field name="name">email.thread.tree</field>
<field name="model">email.thread</field>
<field name="name">mail.thread.tree</field>
<field name="model">mail.thread</field>
<field name="type">tree</field>
<field name="arch" type="xml">
<tree string="Mailgateway Thread">
<tree string="Email Threads">
<field name="message_ids" />
</tree>
</field>
</record>
<!-- Emails action-->
<!-- Emails thread action -->
<record model="ir.actions.act_window" id="action_view_mailgate_thread">
<field name="name">Mailgateway Threads</field>
<field name="res_model">email.thread</field>
<field name="name">Email Threads</field>
<field name="res_model">mail.thread</field>
<field name="view_mode">tree,form</field>
<field name="view_type">form</field>
<field name="view_id" ref="view_mailgate_thread_tree"/>
</record>
<record model="ir.actions.act_window.view" id="action_view_mailgate_thread_view1">
<field name="sequence" eval="1"/>
<field name="view_mode">tree</field>
<field name="view_id" ref="view_mailgate_thread_tree"/>
<field name="act_window_id" ref="action_view_mailgate_thread"/>
</record>
<record model="ir.actions.act_window.view" id="action_view_mailgate_thread_view2">
<field name="sequence" eval="2"/>
<field name="view_mode">form</field>
<field name="view_id" ref="view_mailgate_thread_form"/>
<field name="act_window_id" ref="action_view_mailgate_thread"/>
</record>
<record model="ir.actions.act_window.view" id="action_view_mailgate_thread_view1">
<field name="sequence" eval="1"/>
<field name="view_mode">tree</field>

View File

@ -2,77 +2,70 @@
<openerp>
<data>
<menuitem name="Configuration" parent="base.menu_tools"
id="base.menu_lunch_survey_root" sequence="20"/>
<menuitem name="Configuration" parent="base.menu_tools" id="base.menu_lunch_survey_root" sequence="20"/>
<record model="ir.ui.view" id="view_email_message_form">
<field name="name">email.message.form</field>
<field name="model">email.message</field>
<field name="type">form</field>
<field name="arch" type="xml">
<field name="name">mail.message.form</field>
<field name="model">mail.message</field>
<field name="type">form</field>
<field name="arch" type="xml">
<form string="Email message">
<group colspan="4" col="6">
<field name="subject" widget="char" size="512"/>
<group colspan="4" col="8">
<field name="subject"/>
<field name="date"/>
<field name="user_id" string="User"/>
<field name="partner_id" readonly="1" />
<field name="priority"/>
</group>
<notebook colspan="4">
<page string="Details">
<group col="2" colspan="2">
<separator string="Email Followers" colspan="4"/>
<separator string="Recipients" colspan="4"/>
<field name="email_from"/>
<field name="email_to"/>
<field name="email_cc"/>
<field name="email_bcc" groups="base.group_extended"/>
<field name="reply_to"/>
</group>
<group col="2" colspan="2">
<group col="4" colspan="2">
<separator string="Message Details" colspan="4"/>
<field name="model" readonly="1"/>
<group col="3" colspan="2">
<field name="res_id" readonly="1" groups="base.group_extended"/>
<button name="open_document" string="Open Document" type="object" icon="gtk-jump-to"/>
<field name="message_id" groups="base.group_extended" colspan="4"/>
</group>
<field name="references" widget="char" size="4096" groups="base.group_extended"/>
<field name="model"/>
<button name="open_document" string="Open" type="object" icon="gtk-jump-to" colspan="2"/>
<field name="res_id" groups="base.group_extended"/>
<field name="message_id" groups="base.group_extended" colspan="4"/>
<field name="references" colspan="4" widget="char" size="4096" groups="base.group_extended"/>
</group>
<notebook colspan="4">
<page string="Body (Text)" attrs="{'invisible':[('sub_type','=','html')]}">
<field name="body" colspan="4" widget="text" nolabel="1"/>
</page>
<page string="Body (HTML)" attrs="{'invisible':[('sub_type','=','plain')]}">
<field name="body_html" widget="text_html" nolabel="1" colspan="4"/>
</page>
</notebook>
<page string="Body (Plain)" attrs="{'invisible':[('subtype','=','html')]}">
<field name="body_text" colspan="4" widget="text" nolabel="1"/>
</page>
<page string="Body (Rich)" attrs="{'invisible':[('subtype','=','plain')]}">
<field name="body_html" widget="text_html" nolabel="1" colspan="4"/>
</page>
</notebook>
<separator string="" colspan="4"/>
<group col="6" colspan="4">
<field name="state" colspan="2"/>
<group col="4" colspan="2">
<button name="%(action_email_compose_message_wizard)d" string="Reply" type="action" icon="terp-mail-replied"
context="{'mail':'reply', 'message_id':active_id}" states='received,outgoing,sent,exception,cancel'/>
<button name="send_email" string="Force Send" type="object" icon="gtk-execute" states='outgoing'/>
<button name="process_retry" string="Send Again" type="object" icon="gtk-execute" states='exception,cancel'/>
<button name="do_cancel" string="Cancel" type="object" icon="terp-gtk-stop" states='outgoing'/>
<button name="%(action_email_compose_message_wizard)d" string="Reply" type="action" icon="terp-mail-replied"
context="{'mail':'reply', 'message_id':active_id}" states='received,outgoing,sent,exception,cancel'/>
<button name="send" string="Send Now" type="object" icon="gtk-media-play" states='outgoing'/>
<button name="mark_outgoing" string="Retry" type="object" icon="gtk-redo" states='exception,cancel'/>
<button name="cancel" string="Cancel" type="object" icon="terp-gtk-stop" states='outgoing'/>
</group>
</group>
</page>
<page string="Attachments">
<separator string="Attachments" colspan="4"/>
<field name="attachment_ids" nolabel="1" colspan="4" readonly="1"/>
<field name="attachment_ids" nolabel="1" colspan="4"/>
</page>
<page string="Advanced">
<group col="4" colspan="4">
<field name="smtp_server_id"/>
<field name="sub_type"/>
<field name="debug" groups="base.group_extended"/>
<field name="history"/>
<field name="auto_delete"/>
<separator string="x-headers" colspan="4"/>
<field name="headers" colspan="4" nolabel="1" groups="base.group_extended" height="350"/>
<group col="2" colspan="4">
<field name="mail_server_id"/>
<field name="subtype" groups="base.group_extended"/>
<field name="auto_delete"/>
<field name="headers" colspan="4" groups="base.group_extended" height="350"/>
</group>
</page>
</notebook>
@ -81,39 +74,42 @@
</record>
<record model="ir.ui.view" id="view_email_message_tree">
<field name="name">email.message.tree</field>
<field name="model">email.message</field>
<field name="name">mail.message.tree</field>
<field name="model">mail.message</field>
<field name="type">tree</field>
<field name="arch" type="xml">
<tree string="Emails" colors="grey:state in ('sent', 'cancel');blue:state=='outgoing';red:state=='exception';black:state=='received'">
<tree string="Emails" colors="grey:state in ('sent', 'cancel');blue:state=='outgoing';red:state=='exception';black:state=='received'">
<field name="date"/>
<field name="subject"/>
<field name="email_from"/>
<field name="user_id" string="User"/>
<field name="message_id" string="Message" invisible="1"/>
<field name="message_id" invisible="1"/>
<field name="partner_id" invisible="1"/>
<field name="state"/>
<button name="open_document" string="Open Document" type="object" icon="gtk-jump-to"/>
<button name="open_document" string="Open Related Document" type="object" icon="gtk-jump-to"/>
<button name="open_attachment" string="Open Attachments" type="object" icon="gtk-jump-to"/>
</tree>
</field>
</record>
<record model="ir.ui.view" id="view_email_message_search">
<field name="name">email.message.search</field>
<field name="model">email.message</field>
<field name="name">mail.message.search</field>
<field name="model">mail.message</field>
<field name="type">search</field>
<field name="arch" type="xml">
<search string="Email Search">
<separator orientation="vertical"/>
<filter icon="terp-camera_test" string="Received" domain="[('state','=','received')]"/>
<filter icon="terp-call-start" name="outgoing" string="Outgoing" domain="[('state','=','outgoing')]"/>
<filter icon="terp-gtk-stop" string="Exception" domain="[('state','=','exception')]"/>
<field name="email_from"/>
<field name="email_to"/>
<field name="subject"/>
<field name="date"/>
<newline/>
<group expand="0" string="Extended Filters..." groups="base.group_extended">
<field name="user_id" string="User"/>
<field name="partner_id" string="Partner Name"/>
</group>
<newline/>
<group expand="0" string="Group By..." groups="base.group_extended">
<filter string="State" icon="terp-stock_effects-object-colorize" domain="[]" context="{'group_by':'state'}"/>
@ -130,30 +126,28 @@
<record id="action_view_mail_message" model="ir.actions.act_window">
<field name="name">Messages</field>
<field name="res_model">email.message</field>
<field name="res_model">mail.message</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="domain">[('history', '=', True)]</field>
<field name="domain">[('email_from', '!=', False)]</field>
<field name="context">{'search_default_outgoing':1}</field>
<field name="search_view_id" ref="view_email_message_search"/>
</record>
<act_window domain="[('partner_id', '=', active_id), ('history', '=', True)]"
<act_window domain="[('partner_id', '=', active_id), ('email_from', '!=', False)]"
id="act_res_partner_emails" name="Emails"
res_model="email.message"
res_model="mail.message"
src_model="res.partner"
view_id="view_email_message_tree"/>
<menuitem name="Email" id="menu_email_message_tools"
parent="base.menu_tools" />
<menuitem name="Emails" id="menu_email_message_tools" parent="base.menu_tools" />
<menuitem name="Messages"
id="menu_email_message"
parent="menu_email_message_tools"
action="action_view_mail_message" />
<menuitem name="Email" id="menu_config_email" parent="base.menu_lunch_survey_root" sequence="20"/>
<menuitem name="Emails" id="menu_config_email" parent="base.menu_lunch_survey_root" sequence="20"/>
</data>
</openerp>

View File

@ -26,7 +26,7 @@ class res_partner(osv.osv):
""" Inherits partner and adds CRM information in the partner form """
_inherit = 'res.partner'
_columns = {
'emails': fields.one2many('email.message', 'partner_id', 'Emails', readonly=True, domain=[('history','=',True)]),
'emails': fields.one2many('mail.message', 'partner_id', 'Emails', readonly=True, domain=[('email_from','!=',False)]),
}
res_partner()

View File

@ -2,7 +2,7 @@
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
# Copyright (C) 2009-2010 OpenERP SA (<http://www.openerp.com>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
@ -19,6 +19,4 @@
#
##############################################################################
import openerp_mailgate
import openerp_mailgate

View File

@ -2,7 +2,7 @@
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2004-2010 Tiny SPRL (<http://tiny.be>).
# Copyright (C) 2009-2010 OpenERP SA (<http://www.openerp.com>).
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as

View File

@ -54,7 +54,7 @@ class email_parser(object):
try:
# pass message as bytes because we don't know its encoding until we parse its headers
# and hence can't convert it to utf-8 for transport
res_id = self.rpc('email.thread', 'process_email', self.model, xmlrpclib.Binary(message), custom_values)
res_id = self.rpc('mail.thread', 'process_email', self.model, xmlrpclib.Binary(message), custom_values)
except Exception:
logger = logging.getLogger('mail-gateway')
logger.warning('Failed to process incoming email. Source of the failed mail is available at debug level.', exc_info=True)

View File

@ -1,3 +1,3 @@
"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink"
"access_email_message","email.message","model_email_message",,1,0,0,0
"access_mailgate_thread","email.thread","model_email_thread",,1,0,0,0
"access_mail_message","mail.message","model_mail_message",,1,0,0,0
"access_mail_thread","mail.thread","model_mail_thread",,1,0,0,0

1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_email_message access_mail_message email.message mail.message model_email_message model_mail_message 1 0 0 0
3 access_mailgate_thread access_mail_thread email.thread mail.thread model_email_thread model_mail_thread 1 0 0 0

View File

@ -19,6 +19,6 @@
#
##############################################################################
import email_compose_message
import mail_compose_message
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -1,196 +0,0 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2010-Today OpenERP SA (<http://www.openerp.com>)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
#
##############################################################################
from osv import osv
from osv import fields
import tools
from tools.safe_eval import safe_eval as eval
import re
class email_compose_message(osv.osv_memory):
_name = 'email.compose.message'
_inherit = 'email.message.common'
_description = 'This is the wizard for Compose E-mail'
def default_get(self, cr, uid, fields, context=None):
"""
Returns default values for fields
@param fields: list of fields, for which default values are required to be read
@param context: context arguments, like lang, time zone
@return: Returns a dictionary that contains default values for fields
"""
if context is None:
context = {}
result = super(email_compose_message, self).default_get(cr, uid, fields, context=context)
vals = {}
if context.get('mass_mail'):
return result
if context.get('active_model') and context.get('active_id') and not context.get('mail')=='reply':
vals = self.get_value(cr, uid, context.get('active_model'), context.get('active_id'), context)
elif context.get('mail')=='reply' and context.get('active_id', False):
vals = self.get_message_data(cr, uid, int(context.get('active_id', False)), context)
else:
result['model'] = context.get('active_model', False)
if not vals:
return result
for field in fields:
result.update({field : vals.get(field, False)})
return result
_columns = {
'attachment_ids': fields.many2many('ir.attachment','email_message_send_attachment_rel', 'wizard_id', 'attachment_id', 'Attachments'),
'auto_delete': fields.boolean('Auto Delete', help="Permanently delete emails after sending"),
'filter_id': fields.many2one('ir.filters', 'Filters'),
}
def get_value(self, cr, uid, model, res_id, context=None):
return {}
def get_message_data(self, cr, uid, message_id, context=None):
'''
Called by default_get() to get message detail
@param message_id: Id of the email message
'''
if context is None:
context = {}
result = {}
message_pool = self.pool.get('email.message')
if message_id:
message_data = message_pool.browse(cr, uid, message_id, context)
subject = tools.ustr(message_data and message_data.subject or '')
description = message_data and message_data.body or ''
if context.get('mail','') == 'reply':
header = '-------- Original Message --------'
sender = 'From: %s' % tools.ustr(message_data.email_from or '')
email_to = 'To: %s' % tools.ustr(message_data.email_to or '')
sentdate = 'Date: %s' % message_data.date
desc = '\n > \t %s' % tools.ustr(description.replace('\n', "\n > \t") or '')
description = '\n'.join([header, sender, email_to, sentdate, desc])
if not subject.startswith('Re: '):
subject = "Re: " + subject
result.update({
'body' : description,
'subject' : subject,
'message_id' : message_data and message_data.message_id or False,
'attachment_ids' : [],
'res_id' : message_data and message_data.res_id or False,
'email_from' : message_data and message_data.email_to or False,
'email_to' : message_data and message_data.email_from or False,
'email_cc' : message_data and message_data.email_cc or False,
'email_bcc' : message_data and message_data.email_bcc or False,
'reply_to' : message_data and message_data.reply_to or False,
'model' : message_data and message_data.model or False,
'user_id' : message_data and message_data.user_id and message_data.user_id.id or False,
'references' : message_data and message_data.references and tools.ustr(message_data.references) or False,
'sub_type' : message_data and message_data.sub_type or False,
'headers' : message_data and message_data.headers or False,
'priority' : message_data and message_data.priority or False,
'debug': message_data and message_data.debug or False
})
return result
def send_mail(self, cr, uid, ids, context=None):
'''
Sends the email
'''
if context is None:
context = {}
email_message_pool = self.pool.get('email.message')
attachment = {}
email_ids = []
for mail in self.browse(cr, uid, ids, context=context):
for attach in mail.attachment_ids:
attachment[attach.datas_fname] = attach.datas
references = False
message_id = False
# Reply Email
if context.get('mail') == 'reply' and mail.message_id:
references = mail.references and mail.references + "," + mail.message_id or mail.message_id
else:
message_id = mail.message_id
# Mass mailing
if context.get('mass_mail', False):
if context['active_ids'] and context['active_model']:
active_ids = context['active_ids']
active_model = context['active_model']
else:
active_model = mail.model
active_model_pool = self.pool.get(active_model)
active_ids = active_model_pool.search(cr, uid, eval(mail.filter_id.domain), context=eval(mail.filter_id.context))
for active_id in active_ids:
subject = self.get_template_value(cr, uid, mail.subject, active_model, active_id)
body = self.get_template_value(cr, uid, mail.body, active_model, active_id)
email_to = self.get_template_value(cr, uid, mail.email_to, active_model, active_id)
email_from = self.get_template_value(cr, uid, mail.email_from, active_model, active_id)
email_cc = self.get_template_value(cr, uid, mail.email_cc, active_model, active_id)
email_bcc = self.get_template_value(cr, uid, mail.email_bcc, active_model, active_id)
reply_to = self.get_template_value(cr, uid, mail.reply_to, active_model, active_id)
email_id = email_message_pool.schedule_with_attach(cr, uid, email_from, email_to, subject, body,
model=mail.model, email_cc=email_cc, email_bcc=email_bcc, reply_to=reply_to,
attach=attachment, message_id=message_id, references=references, openobject_id=int(mail.res_id),
subtype=mail.sub_type, x_headers=mail.headers, priority=mail.priority, smtp_server_id=mail.smtp_server_id and mail.smtp_server_id.id,
auto_delete=mail.auto_delete or False, context=context)
email_ids.append(email_id)
else:
email_id = email_message_pool.schedule_with_attach(cr, uid, mail.email_from, mail.email_to, mail.subject, mail.body,
model=mail.model, email_cc=mail.email_cc, email_bcc=mail.email_bcc, reply_to=mail.reply_to,
attach=attachment, message_id=message_id, references=references, openobject_id=int(mail.res_id),
subtype=mail.sub_type, x_headers=mail.headers, priority=mail.priority, smtp_server_id=mail.smtp_server_id and mail.smtp_server_id.id,
auto_delete=mail.auto_delete, context=context)
email_ids.append(email_id)
return {'type': 'ir.actions.act_window_close'}
def get_template_value(self, cr, uid, message, model, resource_id, context=None):
if context is None:
context = {}
def merge(match):
exp = str(match.group()[2:-1]).strip()
result = eval(exp,
{
'user' : self.pool.get('res.users').browse(cr, uid, uid, context=context),
'context': dict(context), # copy context to prevent side-effects of eval
'object' : self.pool.get(model).browse(cr, uid, resource_id),
})
if result in (None, False):
return str("")
return tools.ustr(result)
com = re.compile('(\$\{.+?\})')
return message and com.sub(merge, message)
email_compose_message()
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -0,0 +1,248 @@
# -*- coding: utf-8 -*-
##############################################################################
#
# OpenERP, Open Source Management Solution
# Copyright (C) 2010-Today OpenERP SA (<http://www.openerp.com>)
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>
#
##############################################################################
import re
import tools
from mail.mail_message import to_email
from osv import osv
from osv import fields
from tools.safe_eval import safe_eval as eval
from tools.safe_eval import literal_eval
from tools.translate import _
# main mako-like expression pattern
EXPRESSION_PATTERN = re.compile('(\$\{.+?\})')
class mail_compose_message(osv.osv_memory):
"""Generic E-mail composition wizard. This wizard is meant to be inherited
at model and view level to provide specific wizard features.
The behavior of the wizard can be modified through the use of context
parameters, among which are:
* mass_mail: turns multi-recipient mode, where the mail details can
contain template placeholders that will be merged with
actual data before being sent to each recipient, as
determined via ``context['active_model']`` and
``context['active_ids']``.
* mail: if set to 'reply', the wizard will be in mail reply mode
* active_model: model name of the document to which the mail being
composed is related
* active_id: id of the document to which the mail being composed is
related, or id of the message to which user is replying,
in case mail == 'reply'.
* active_ids: ids of the documents to which the mail being composed is
related, in case ``context['mass_mail']`` is set.
"""
_name = 'mail.compose.message'
_inherit = 'mail.message.common'
_description = 'E-mail composition wizard'
def default_get(self, cr, uid, fields, context=None):
"""Overridden to provide specific defaults depending on the context
parameters.
:param dict context: several context values will modify the behavior
of the wizard, cfr. the class description.
"""
if context is None:
context = {}
result = super(mail_compose_message, self).default_get(cr, uid, fields, context=context)
vals = {}
if context.get('mass_mail'):
return result
if context.get('active_model') and context.get('active_id') and not context.get('mail')=='reply':
vals = self.get_value(cr, uid, context.get('active_model'), context.get('active_id'), context)
elif context.get('mail')=='reply' and context.get('active_id'):
vals = self.get_message_data(cr, uid, int(context['active_id']), context)
else:
result['model'] = context.get('active_model', False)
if not vals:
return result
for field in fields:
result.update({field : vals.get(field, False)})
return result
_columns = {
'attachment_ids': fields.many2many('ir.attachment','email_message_send_attachment_rel', 'wizard_id', 'attachment_id', 'Attachments'),
'auto_delete': fields.boolean('Auto Delete', help="Permanently delete emails after sending"),
'filter_id': fields.many2one('ir.filters', 'Filters'),
}
def get_value(self, cr, uid, model, res_id, context=None):
"""Returns a defaults-like dict with initial values for the composition
wizard when sending an email related to the document record identified
by ``model`` and ``res_id``.
The default implementation returns an empty dictionary, and is meant
to be overridden by subclasses.
:param str model: model name of the document record this mail is related to.
:param int res_id: id of the document record this mail is related to.
:param dict context: several context values will modify the behavior
of the wizard, cfr. the class description.
"""
return {}
def get_message_data(self, cr, uid, message_id, context=None):
"""Returns a defaults-like dict with initial values for the composition
wizard when replying to the given message (e.g. including the quote
of the initial message, and the correct recipient).
Should not be called unless ``context['mail'] == 'reply'``.
:param int message_id: id of the mail.message to which the user
is replying.
:param dict context: several context values will modify the behavior
of the wizard, cfr. the class description.
When calling this method, the ``'mail'`` value
in the context should be ``'reply'``.
"""
if context is None:
context = {}
result = {}
mail_message = self.pool.get('mail.message')
if message_id:
message_data = mail_message.browse(cr, uid, message_id, context)
subject = tools.ustr(message_data.subject or '')
# we use the plain text version of the original mail, by default,
# as it is easier to quote than the HTML version.
# XXX TODO: make it possible to switch to HTML on the fly
description = message_data.body_text or ''
if context.get('mail') == 'reply':
header = _('-------- Original Message --------')
sender = _('From: %s') % tools.ustr(message_data.email_from or '')
email_to = _('To: %s') % tools.ustr(message_data.email_to or '')
sentdate = _('Date: %s') % message_data.date
desc = '\n > \t %s' % tools.ustr(description.replace('\n', "\n > \t") or '')
description = '\n'.join([header, sender, email_to, sentdate, desc])
re_prefix = _("Re:")
if not (subject.startswith('Re:') or subject.startswith(re_prefix)):
subject = "%s %s" % (re_prefix, subject)
result.update({
'subtype' : 'plain', # default to the text version due to quoting
'body_text' : description,
'subject' : subject,
'message_id' : message_data.message_id or False,
'attachment_ids' : [],
'res_id' : message_data.res_id or False,
'email_from' : message_data.email_to or False,
'email_to' : message_data.email_from or False,
'email_cc' : message_data.email_cc or False,
'email_bcc' : message_data.email_bcc or False,
'reply_to' : message_data.reply_to or False,
'model' : message_data.model or False,
'user_id' : message_data.user_id and message_data.user_id.id or False,
'references' : message_data.references and tools.ustr(message_data.references) or False,
'headers' : message_data.headers or False,
})
return result
def send_mail(self, cr, uid, ids, context=None):
'''Process the wizard contents and proceed with sending the corresponding
email(s), rendering any template patterns on the fly if needed.
The resulting email(s) are scheduled for being sent the next time the
mail.message scheduler runs, or the next time
``mail.message.process_email_queue`` is called.
:param dict context: several context values will modify the behavior
of the wizard, cfr. the class description.
'''
if context is None:
context = {}
mail_message = self.pool.get('mail.message')
for mail in self.browse(cr, uid, ids, context=context):
attachment = {}
for attach in mail.attachment_ids:
attachment[attach.datas_fname] = attach.datas
references = False
message_id = False
# Reply Email
if context.get('mail') == 'reply' and mail.message_id:
references = mail.references and mail.references + "," + mail.message_id or mail.message_id
else:
message_id = mail.message_id
if context.get('mass_mail'):
# Mass mailing: must render the template patterns
if context.get('active_ids') and context.get('active_model'):
active_ids = context['active_ids']
active_model = context['active_model']
else:
active_model = mail.model
active_model_pool = self.pool.get(active_model)
active_ids = active_model_pool.search(cr, uid, literal_eval(mail.filter_id.domain), context=literal_eval(mail.filter_id.context))
for active_id in active_ids:
subject = self.render_template(cr, uid, mail.subject, active_model, active_id)
body = mail.body_html if mail.subtype == 'html' else mail.body_text
body = self.render_template(cr, uid, body, active_model, active_id)
email_from = self.render_template(cr, uid, mail.email_from, active_model, active_id)
email_to = self.render_template(cr, uid, mail.email_to, active_model, active_id)
email_cc = self.render_template(cr, uid, mail.email_cc, active_model, active_id)
email_bcc = self.render_template(cr, uid, mail.email_bcc, active_model, active_id)
reply_to = self.render_template(cr, uid, mail.reply_to, active_model, active_id)
mail_message.schedule_with_attach(cr, uid, email_from, to_email(email_to), subject, body,
model=mail.model, email_cc=to_email(email_cc), email_bcc=to_email(email_bcc), reply_to=reply_to,
attachments=attachment, message_id=message_id, references=references, res_id=int(mail.res_id),
subtype=mail.subtype, headers=mail.headers, auto_delete=mail.auto_delete, context=context)
else:
# normal mode - no mass-mailing
mail_message.schedule_with_attach(cr, uid, mail.email_from, to_email(mail.email_to), mail.subject, mail.body,
model=mail.model, email_cc=to_email(mail.email_cc), email_bcc=to_email(mail.email_bcc), reply_to=mail.reply_to,
attachments=attachment, message_id=message_id, references=references, res_id=int(mail.res_id),
subtype=mail.subtype, headers=mail.headers, auto_delete=mail.auto_delete, context=context)
return {'type': 'ir.actions.act_window_close'}
def render_template(self, cr, uid, template, model, res_id, context=None):
"""Render the given template text, replace mako-like expressions ``${expr}``
with the result of passing these expressions ``through safe_eval()`` with
an evaluation context containing:
* ``user``: browse_record of the current user
* ``object``: browse_record of the document record this mail is
related to
* ``context``: the context passed to the mail composition wizard
:param str template: the template text to render
:param str model: model name of the document record this mail is related to.
:param int res_id: id of the document record this mail is related to.
"""
if context is None:
context = {}
def merge(match):
exp = str(match.group()[2:-1]).strip()
result = eval(exp,
{
'user' : self.pool.get('res.users').browse(cr, uid, uid, context=context),
'object' : self.pool.get(model).browse(cr, uid, res_id, context=context),
'context': dict(context), # copy context to prevent side-effects of eval
})
if result in (None, False):
return ""
return tools.ustr(result)
return template and EXPRESSION_PATTERN.sub(merge, template)
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -3,15 +3,14 @@
<data>
<record model="ir.ui.view" id="email_compose_message_wizard_form">
<field name="name">email.compose.message.form</field>
<field name="model">email.compose.message</field>
<field name="name">mail.compose.message.form</field>
<field name="model">mail.compose.message</field>
<field name="type">form</field>
<field name="arch" type="xml">
<form string="Compose Email">
<group col="6" colspan="4">
<field name="model" invisible="context.get('active_model',False)"/>
<field name='filter_id' invisible="context.get('active_model',False)"/>
<field name="smtp_server_id" widget="selection" colspan="4" invisible="1"/>
<field name="email_from" colspan="4" required="1"/>
<field name="email_to" colspan="4" required="1"/>
<field name="email_cc" colspan="4"/>
@ -23,7 +22,7 @@
<separator string="" colspan="4"/>
<notebook colspan="4">
<page string="Body">
<field name="body" colspan="4" nolabel="1"/>
<field name="body_text" colspan="4" nolabel="1"/>
</page>
<page string="Attachments">
<label string="Add here all attachments of the current document you want to include in the Email." colspan="4"/>
@ -41,16 +40,17 @@
<record id="action_email_compose_message_wizard" model="ir.actions.act_window">
<field name="name">Compose E-mail</field>
<field name="res_model">email.compose.message</field>
<field name="src_model">email.compose.message</field>
<field name="res_model">mail.compose.message</field>
<field name="src_model">mail.compose.message</field>
<field name="type">ir.actions.act_window</field>
<field name="view_type">form</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
<act_window name="Mass Mailing"
res_model="email.compose.message"
<!-- Replace the default mass-mailing wizard in base with the composition wizard -->
<act_window name="Mass Mailing"
res_model="mail.compose.message"
src_model="res.partner"
view_mode="form"
target="new"