##############################################################################
import base64
+import dateutil.parser
import email
import logging
import re
import time
from email.header import decode_header
+from email.message import Message
import tools
from osv import osv
'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_from': fields.char('From', size=128, help='Message sender, taken from user preferences'),
'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),
+ 'reply_to':fields.char('Reply-To', size=256, help='Preferred response address for the message'),
+ 'headers': fields.text('Message headers', readonly=1,
+ help="Full message headers, e.g. SMTP session headers "
+ "(usually available on inbound messages only)"),
'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 = {
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)
+ if message.body_text:
+ msg_txt += truncate_text(message.body_text)
else:
msg_txt = (message.user_id.name or '/') + _(' on ') + format_date_tz(message.date, tz) + ':\n\t'
msg_txt += message.subject
('outgoing', 'Outgoing'),
('sent', 'Sent'),
('received', 'Received'),
- ('exception', 'Exception'),
+ ('exception', 'Delivery Failed'),
('cancel', 'Cancelled'),
], 'State', readonly=True),
- 'auto_delete': fields.boolean('Auto Delete', help="Permanently delete this email after sending it"),
+ 'auto_delete': fields.boolean('Auto Delete', help="Permanently delete this email after sending it, to save space"),
+ 'original': fields.binary('Original', help="Original version of the message, as it was sent on the network", readonly=1),
+ }
+
+ _defaults = {
+ 'state': 'received',
}
def init(self, cr):
if not cr.fetchone():
cr.execute("""CREATE INDEX mail_message_model_res_id_idx ON mail_message (model, res_id)""")
+ def copy(self, cr, uid, id, default=None, context=None):
+ """Overridden to avoid duplicating fields that are unique to each email"""
+ if default is None:
+ default = {}
+ default.update(message_id=False,original=False,headers=False)
+ return super(mail_message,self).copy(cr, uid, id, default=default, context=context)
+
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,
: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
+ :param dict attachments: map of filename to filecontents, 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.)
'user_id': uid,
'model': model,
'res_id': res_id,
- 'body_text': body if subtype == 'plain' else False,
+ 'body_text': body if subtype != 'html' else False,
'body_html': body if subtype == 'html' else False,
'email_from': email_from,
'email_to': email_to and ','.join(email_to) or '',
}
email_msg_id = self.create(cr, uid, msg_vals, context)
attachment_ids = []
- for fname, fcontent in attachments.items():
+ for fname, fcontent in attachments.iteritems():
attachment_data = {
'name': fname,
'datas_fname': fname,
_logger.exception("Failed processing mail queue")
return res
- def parse_message(self, message):
+ def parse_message(self, message, save_original=False):
"""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
+ :param bool save_original: whether the returned dict
+ should include an ``original`` entry with the base64
+ encoded source of the message.
:rtype: dict
:return: A dict with the following structure, where each
field may not be present if missing in original
#.. all X- headers...
},
'subtype': msg_mime_subtype,
- 'body': plaintext_body
+ 'body_text': plaintext_body
'body_html': html_body,
'attachments': { 'file1': 'bytes',
'file2': 'bytes' }
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 save_original:
+ # save original, we need to be able to read the original email sometimes
+ msg['original'] = message.as_string() if isinstance(message, Message) \
+ else message
+ msg['original'] = base64.b64encode(msg['original']) # binary fields are b64
if not message_id:
# Very unusual situation, be we should be fault-tolerant here
if 'From' in fields:
msg['from'] = decode(msg_txt.get('From') or msg_txt.get_unixfrom())
+ if 'To' in fields:
+ msg['to'] = decode(msg_txt.get('To'))
if 'Delivered-To' in fields:
msg['to'] = decode(msg_txt.get('Delivered-To'))
msg['reply'] = decode(msg_txt.get('Reply-To'))
if 'Date' in fields:
- msg['date'] = decode(msg_txt.get('Date'))
+ date_hdr = decode(msg_txt.get('Date'))
+ msg['date'] = dateutil.parser.parse(date_hdr).strftime("%Y-%m-%d %H:%M:%S")
if 'Content-Transfer-Encoding' in fields:
msg['encoding'] = msg_txt.get('Content-Transfer-Encoding')
msg['in-reply-to'] = msg_txt.get('In-Reply-To')
msg['headers'] = {}
+ msg['subtype'] = 'plain'
for item in msg_txt.items():
if item[0].startswith('X-'):
msg['headers'].update({item[0]: item[1]})
msg['body_html'] = body
msg['subtype'] = 'html'
body = tools.html2plaintext(body)
- else:
- msg['subtype'] = 'plain'
- msg['body'] = tools.ustr(body, encoding)
+ msg['body_text'] = tools.ustr(body, encoding)
attachments = {}
if msg_txt.is_multipart() or 'multipart/alternative' in msg.get('content-type', ''):
content = tools.ustr(content, encoding)
if part.get_content_subtype() == 'html':
msg['body_html'] = content
+ msg['subtype'] = 'html' # html version prevails
body = tools.ustr(tools.html2plaintext(content))
elif part.get_content_subtype() == 'plain':
body = content
res = part.get_payload(decode=True)
body += tools.ustr(res, encoding)
- msg['body'] = body
- msg['attachments'] = attachments
+ msg['body_text'] = body
+ msg['attachments'] = attachments
+
+ # for backwards compatibility:
+ msg['body'] = msg['body_text']
+ msg['sub_type'] = msg['subtype'] or 'plain'
return msg
def send(self, cr, uid, ids, auto_commit=False, context=None):